summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2024-02-12 12:55:11 +0100
committerMel <einebeere@gmail.com>2024-02-12 12:55:11 +0100
commitd2b5fc5b3bc648afffa42375706429685ac63794 (patch)
treea2dfbb241e1d46e5616c5884e5f3d685de2a2cb6 /src
parent588c7e87b7cab270698d43ca5c22d67793ae5fc4 (diff)
downloadmeowcraft-d2b5fc5b3bc648afffa42375706429685ac63794.tar.zst
meowcraft-d2b5fc5b3bc648afffa42375706429685ac63794.zip
Split rendering into own thread and sync through render action lists
Diffstat (limited to 'src')
-rw-r--r--src/Assets.cpp36
-rw-r--r--src/Assets.hpp10
-rw-r--r--src/Defines.hpp13
-rw-r--r--src/Entities/Player.cpp74
-rw-r--r--src/Entities/Player.hpp18
-rw-r--r--src/GFX/Actions.hpp39
-rw-r--r--src/GFX/Resources.cpp26
-rw-r--r--src/GFX/Resources.hpp38
-rw-r--r--src/GFX/Shading/Program.cpp9
-rw-r--r--src/GFX/Shading/Program.hpp4
-rw-r--r--src/GFX/Shading/Shader.hpp8
-rw-r--r--src/GFX/Window.cpp18
-rw-r--r--src/GFX/Window.hpp6
-rw-r--r--src/Game.cpp81
-rw-r--r--src/Game.hpp20
-rw-r--r--src/Math/Rotation.hpp4
-rw-r--r--src/Render.cpp102
-rw-r--r--src/Render.hpp88
-rw-r--r--src/Util/ImageViewer.cpp68
-rw-r--r--src/Util/ImageViewer.hpp12
-rw-r--r--src/World/Clouds.cpp113
-rw-r--r--src/World/Clouds.hpp22
-rw-r--r--src/main.cpp167
23 files changed, 557 insertions, 419 deletions
diff --git a/src/Assets.cpp b/src/Assets.cpp
index 66fdc88..dc2495e 100644
--- a/src/Assets.cpp
+++ b/src/Assets.cpp
@@ -2,16 +2,40 @@
 
 namespace MC::Assets {
 
-const Char* Shaders::fragment =
-#include "../assets/generated/shaders/fragment.glsl.includable"
+Char const* Shaders::terrain::vertex =
+#include "../assets/generated/shaders/terrain.vert.glsl.include"
 ;
 
-const Char* Shaders::vertex =
-#include "../assets/generated/shaders/vertex.glsl.includable"
+Char const* Shaders::terrain::fragment =
+#include "../assets/generated/shaders/terrain.frag.glsl.include"
 ;
 
-const Char* Images::atlas =
-#include "../assets/generated/images/atlas.ppm.includable"
+Char const* Shaders::clouds::vertex =
+#include "../assets/generated/shaders/clouds.vert.glsl.include"
+;
+
+Char const* Shaders::clouds::fragment =
+#include "../assets/generated/shaders/clouds.frag.glsl.include"
+;
+
+Char const* Shaders::image_viewer::vertex =
+#include "../assets/generated/shaders/image_viewer.vert.glsl.include"
+;
+
+Char const* Shaders::image_viewer::fragment =
+#include "../assets/generated/shaders/image_viewer.frag.glsl.include"
+;
+
+Char const* Shaders::block_outline::vertex =
+#include "../assets/generated/shaders/block_outline.vert.glsl.include"
+;
+
+Char const* Shaders::block_outline::fragment =
+#include "../assets/generated/shaders/block_outline.frag.glsl.include"
+;
+
+Char const* Images::atlas =
+#include "../assets/generated/images/atlas.ppm.include"
 ;
 
 }
\ No newline at end of file
diff --git a/src/Assets.hpp b/src/Assets.hpp
index 9ff5c42..f4b2f46 100644
--- a/src/Assets.hpp
+++ b/src/Assets.hpp
@@ -6,14 +6,18 @@ namespace MC::Assets {
 
 namespace Shaders {
 
-extern const Char* vertex;
-extern const Char* fragment;
+#define MC_ASSETS_SHADER(name) namespace name { extern Char const* vertex; extern Char const* fragment; }
+
+MC_ASSETS_SHADER(terrain)
+MC_ASSETS_SHADER(clouds)
+MC_ASSETS_SHADER(block_outline)
+MC_ASSETS_SHADER(image_viewer)
 
 }
 
 namespace Images {
 
-extern const Char* atlas;
+extern Char const* atlas;
 
 }
 
diff --git a/src/Defines.hpp b/src/Defines.hpp
new file mode 100644
index 0000000..1280e93
--- /dev/null
+++ b/src/Defines.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#define APP_NAME "Meowcraft"
+
+#define WINDOW_WIDTH 800
+#define WINDOW_HEIGHT 600
+#define ASPECT (static_cast<Real>(WINDOW_WIDTH) / WINDOW_HEIGHT)
+
+#define FOV 90
+
+// #DBDBDB
+#define SKY_COLOR {0.85, 0.85, 0.85}
+#define SUN_DIRECTION {1, -1, 0}
diff --git a/src/Entities/Player.cpp b/src/Entities/Player.cpp
index b7bee24..652f3fd 100644
--- a/src/Entities/Player.cpp
+++ b/src/Entities/Player.cpp
@@ -1,29 +1,8 @@
 #include "Player.hpp"
 #include "../Common/Casts.hpp"
-#include "../Math/MVP.hpp"
 #include <unordered_set>
 
 namespace MC::Entities {
-Player::Player(Position::World position, Real ascept, Real fov, Real near, Real far)
-    : m_transform(position),
-    m_outline_program(
-        {GFX::Shading::Shader::Type::Vertex, outline_vertex},
-        {GFX::Shading::Shader::Type::Fragment, outline_fragment}
-    ),
-    m_outline_mesh(create_outline_cube_mesh()),
-    m_outline_model_uniform(), m_outline_view_uniform(), m_outline_projection_uniform() {
-    m_outline_program.bind();
-
-    m_outline_model_uniform = m_outline_program.uniform("model_matrix");
-    m_outline_view_uniform = m_outline_program.uniform("view_matrix");
-    m_outline_projection_uniform = m_outline_program.uniform("projection_matrix");
-
-    m_outline_model_uniform.set(Math::MVP::model<F32>({}, {}, {}));
-    m_outline_view_uniform.set(Math::MVP::view<F32>({}, {}));
-    m_outline_projection_uniform.set(Math::MVP::perspective_projection<F32>(ascept, fov, near, far));
-
-    m_outline_program.unbind();
-}
 
 void Player::update(const Time& time, GFX::Window& window, GFX::Camera& camera, World::World& world) {
     auto const input_direction = directional_input(window);
@@ -51,24 +30,16 @@ void Player::update(const Time& time, GFX::Window& window, GFX::Camera& camera,
     actions(window, world);
 }
 
-// TODO: Proof-of-concept, will all be moved to the rendering system.
-void Player::render(const GFX::Camera& camera) {
-    // Render currently targeted block outline.
-
+void Player::render(GFX::Actions& actions) {
     if (m_targeted_block.has_value()) {
-        auto targeted_block = m_targeted_block.value();
-
-        m_outline_program.bind();
-
-        m_outline_view_uniform.set(Math::MVP::view<F32>(camera.position(), camera.angles()));
-        m_outline_model_uniform.set(Math::MVP::model<F32>(Vec3(targeted_block.position), Vec3::one(), {}));
-
-        m_outline_mesh.bind();
-        // TODO: These are very thin lines, do we have any better easy options?
-        glDrawElements(GL_LINES, m_outline_mesh.size(), GL_UNSIGNED_INT, nullptr);
-        m_outline_mesh.unbind();
-
-        m_outline_program.unbind();
+        auto position = Vec3(m_targeted_block->position);
+        actions.add({
+            .mesh = &m_outline_mesh,
+            .program = GFX::Resources::Program::BlockOutline,
+            .transform = Transform{position},
+            // TODO: These are very thin lines, do we have any better easy options?
+            .draw_mode = GFX::DrawMode::Lines,
+        });
     }
 }
 
@@ -371,31 +342,4 @@ GFX::Mesh Player::create_outline_cube_mesh() {
     return builder.mesh();
 }
 
-const Char* Player::outline_vertex = R"v(
-#version 330 core
-
-uniform mat4 model_matrix;
-uniform mat4 view_matrix;
-uniform mat4 projection_matrix;
-
-layout (location = 0) in vec3 position;
-
-void main() {
-    vec4 world_position = model_matrix * vec4(position, 1.0);
-    vec4 view_position = view_matrix * world_position;
-    vec4 clip_position = projection_matrix * view_position;
-
-    gl_Position = clip_position;
-}
-)v";
-
-const Char* Player::outline_fragment = R"f(
-#version 330 core
-
-out vec4 color;
-
-void main() {
-    color = vec4(0.0, 0.0, 0.0, 1.0);
-}
-)f";
 }
diff --git a/src/Entities/Player.hpp b/src/Entities/Player.hpp
index c829d1f..83ac091 100644
--- a/src/Entities/Player.hpp
+++ b/src/Entities/Player.hpp
@@ -2,10 +2,10 @@
 
 #include "../Time.hpp"
 #include "../Transform.hpp"
+#include "../GFX/Actions.hpp"
 #include "../GFX/Camera.hpp"
 #include "../World/World.hpp"
 #include "../GFX/Window.hpp"
-#include "../GFX/Shading/Program.hpp"
 #include "../Math/AABB.hpp"
 #include "../Math/Rotation.hpp"
 #include "../World/Position.hpp"
@@ -13,10 +13,13 @@
 namespace MC::Entities {
 class Player {
 public:
-    explicit Player(Position::World position, Real ascept, Real fov, Real near, Real far);
+    explicit Player(Position::World position)
+        : m_transform(position), m_outline_mesh(create_outline_cube_mesh()) {}
+
+    Position::World position() const { return m_transform.position(); }
 
     void update(const Time& time, GFX::Window& window, GFX::Camera& camera, World::World& world);
-    void render(const GFX::Camera& camera);
+    void render(GFX::Actions& actions);
 
     void move(Position::WorldOffset by);
     void move_to(Position::World to);
@@ -83,16 +86,7 @@ private:
     Transform m_transform;
     static inline AABB s_bounds{{0.35, 1.8, 0.35}};
 
-    // TODO: Put this into the rendering system
-    static const Char* outline_vertex;
-    static const Char* outline_fragment;
-
-    GFX::Shading::Program m_outline_program;
     GFX::Mesh m_outline_mesh;
-
-    GFX::Shading::Uniform m_outline_model_uniform;
-    GFX::Shading::Uniform m_outline_view_uniform;
-    GFX::Shading::Uniform m_outline_projection_uniform;
 };
 
 }
diff --git a/src/GFX/Actions.hpp b/src/GFX/Actions.hpp
new file mode 100644
index 0000000..cc19169
--- /dev/null
+++ b/src/GFX/Actions.hpp
@@ -0,0 +1,39 @@
+#pragma once
+#include "Mesh.hpp"
+#include "Resources.hpp"
+#include "../Transform.hpp"
+
+namespace MC::GFX {
+
+enum class DrawMode {
+    Triangles = GL_TRIANGLES,
+    Lines = GL_LINES,
+};
+
+struct Action {
+    Mesh* mesh;
+    Resources::Program program;
+    Transform transform;
+
+    F32 alpha = 1.0f;
+    DrawMode draw_mode = DrawMode::Triangles;
+};
+
+class Actions {
+public:
+    void add(Action const& action) {
+        m_actions.push_back(action);
+    }
+
+    void clear() {
+        m_actions.clear();
+    }
+
+    const std::vector<Action>& actions() const {
+        return m_actions;
+    }
+private:
+    std::vector<Action> m_actions;
+};
+
+}
diff --git a/src/GFX/Resources.cpp b/src/GFX/Resources.cpp
new file mode 100644
index 0000000..34ce32e
--- /dev/null
+++ b/src/GFX/Resources.cpp
@@ -0,0 +1,26 @@
+#include "Resources.hpp"
+
+#include "../Common/Assert.hpp"
+
+namespace MC::GFX {
+
+void Resources::initialize() {
+    ASSERT(!m_initialized, "Resources already initialized");
+    for (auto& metadata: m_program_metadata) {
+        m_programs[static_cast<USize>(metadata.id)] = create_program(metadata);
+    }
+    m_initialized = true;
+}
+
+Shading::Program const& Resources::program(Program id) const {
+    ASSERT(m_initialized, "Resources not initialized");
+    return m_programs[static_cast<USize>(id)];
+}
+
+Shading::Program Resources::create_program(ProgramMetadata const& metadata) {
+    Shading::Shader vertex(Shading::Shader::Type::Vertex, metadata.vertex);
+    Shading::Shader fragment(Shading::Shader::Type::Fragment, metadata.fragment);
+    return {vertex, fragment};
+}
+
+}
diff --git a/src/GFX/Resources.hpp b/src/GFX/Resources.hpp
new file mode 100644
index 0000000..4206e33
--- /dev/null
+++ b/src/GFX/Resources.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <array>
+#include "Shading/Program.hpp"
+
+namespace MC::GFX {
+class Resources {
+public:
+    enum class Program: USize {
+        Terrain,
+        Clouds,
+        ImageViewer,
+        BlockOutline,
+    };
+    constexpr static USize ProgramCount = 4;
+
+    void initialize();
+    Shading::Program const& program(Program id) const;
+private:
+    struct ProgramMetadata {
+        Program id;
+        Char const* vertex;
+        Char const* fragment;
+    };
+
+    static inline std::array<ProgramMetadata, ProgramCount> const m_program_metadata = {{
+        {Program::Terrain, Assets::Shaders::terrain::vertex, Assets::Shaders::terrain::fragment},
+        {Program::Clouds, Assets::Shaders::clouds::vertex, Assets::Shaders::clouds::fragment},
+        {Program::ImageViewer, Assets::Shaders::image_viewer::vertex, Assets::Shaders::image_viewer::fragment},
+        {Program::BlockOutline, Assets::Shaders::block_outline::vertex, Assets::Shaders::block_outline::fragment},
+    }};
+
+    Bool m_initialized = false;
+    std::array<Shading::Program, ProgramCount> m_programs = {};
+
+    static Shading::Program create_program(ProgramMetadata const& metadata);
+};
+}
diff --git a/src/GFX/Shading/Program.cpp b/src/GFX/Shading/Program.cpp
index ff10012..fe5d576 100644
--- a/src/GFX/Shading/Program.cpp
+++ b/src/GFX/Shading/Program.cpp
@@ -2,6 +2,8 @@
 #include <stdexcept>
 #include "Program.hpp"
 
+#include "../../Common/Assert.hpp"
+
 namespace MC::GFX::Shading {
 
 Program::Program(Shader vertex, Shader fragment) {
@@ -26,6 +28,7 @@ Program::Program(Shader vertex, Shader fragment) {
 }
 
 void Program::bind() const {
+    ASSERT(m_program != 0, "Program is not initialized");
     glUseProgram(m_program);
 }
 
@@ -33,10 +36,12 @@ void Program::unbind() const {
     glUseProgram(0);
 }
 
-Uniform Program::uniform(const std::string& name) const {
+std::optional<Uniform> Program::uniform(const std::string& name) const {
+    ASSERT(m_program != 0, "Program is not initialized");
     auto index = glGetUniformLocation(m_program, name.c_str());
 
-    return {name, static_cast<U32>(index)};
+    if (index == -1) return {};
+    return {{name, static_cast<U32>(index)}};
 }
 
 U32 Program::get() const {
diff --git a/src/GFX/Shading/Program.hpp b/src/GFX/Shading/Program.hpp
index 2f48698..67838dc 100644
--- a/src/GFX/Shading/Program.hpp
+++ b/src/GFX/Shading/Program.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <optional>
 #include <string>
 #include "Shader.hpp"
 #include "Uniform.hpp"
@@ -8,11 +9,12 @@ namespace MC::GFX::Shading {
 
 class Program {
 public:
+    Program() : m_program(0) {}
     Program(Shader vertex, Shader fragment);
 
     U32 get() const;
 
-    Uniform uniform(const std::string& name) const;
+    std::optional<Uniform> uniform(std::string const& name) const;
 
     void bind() const;
     void unbind() const;
diff --git a/src/GFX/Shading/Shader.hpp b/src/GFX/Shading/Shader.hpp
index 21fd899..8c6c5c8 100644
--- a/src/GFX/Shading/Shader.hpp
+++ b/src/GFX/Shading/Shader.hpp
@@ -19,14 +19,6 @@ public:
         return m_shader;
     }
 
-    static Shader create_vertex() {
-        return {Type::Vertex, Assets::Shaders::vertex};
-    }
-
-    static Shader create_fragment() {
-        return {Type::Fragment, Assets::Shaders::fragment};
-    }
-
 private:
     U32 m_shader;
 };
diff --git a/src/GFX/Window.cpp b/src/GFX/Window.cpp
index bbe2ba7..c0c5b03 100644
--- a/src/GFX/Window.cpp
+++ b/src/GFX/Window.cpp
@@ -2,6 +2,8 @@
 #include "../Common/Sizes.hpp"
 #include "Window.hpp"
 
+#include "../Common/Assert.hpp"
+
 namespace MC::GFX {
 
 Window::Window(const Char* title, U32 width, U32 height) {
@@ -48,8 +50,11 @@ Bool Window::mouse(I32 key, I32 type) const {
     return glfwGetMouseButton(m_window, key) == type;
 }
 
-void Window::start_frame() {
+void Window::start_render() {
     glfwSwapBuffers(m_window);
+}
+
+void Window::poll_events() {
     glfwPollEvents();
 }
 
@@ -57,4 +62,13 @@ void Window::on_size_change(void (callback)(GLFWwindow*, I32, I32)) {
     glfwSetFramebufferSizeCallback(m_window, callback);
 }
 
-}
\ No newline at end of file
+void Window::attach() const {
+    glfwMakeContextCurrent(m_window);
+}
+
+void Window::detach() const {
+    ASSERT(glfwGetCurrentContext() == m_window, "Cannot detach window that is not current");
+    glfwMakeContextCurrent(nullptr);
+}
+
+}
diff --git a/src/GFX/Window.hpp b/src/GFX/Window.hpp
index ac0efd2..c26b0fd 100644
--- a/src/GFX/Window.hpp
+++ b/src/GFX/Window.hpp
@@ -16,8 +16,12 @@ public:
 
     void on_size_change(void (* callback)(GLFWwindow*, I32, I32));
 
+    void attach() const;
+    void detach() const;
+
     void close();
-    void start_frame();
+    void start_render();
+    void poll_events();
     Vector<2> mouse_delta();
 
     Bool key(I32 key, I32 type) const;
diff --git a/src/Game.cpp b/src/Game.cpp
new file mode 100644
index 0000000..117ba09
--- /dev/null
+++ b/src/Game.cpp
@@ -0,0 +1,81 @@
+#include "Game.hpp"
+
+#include <GLFW/glfw3.h>
+#include "Time.hpp"
+#include "Common/Sizes.hpp"
+#include "Entities/Player.hpp"
+#include "GFX/Camera.hpp"
+#include "GFX/Window.hpp"
+#include "World/Clouds.hpp"
+#include "World/World.hpp"
+
+namespace MC {
+
+void Game::run() const {
+    World::World world{};
+
+    GFX::Camera camera{};
+
+    World::Clouds clouds{};
+    Entities::Player player{{0, World::Chunk::Height / 2.0, 0}};
+
+    Time time;
+
+    while (!m_window.should_close()) {
+        m_window.poll_events();
+
+#ifdef __APPLE__
+        // Needs to happen on the main thread
+        fix_macos_render();
+#endif
+
+        time.start_frame();
+
+        if (m_window.key(GLFW_KEY_ESCAPE, GLFW_PRESS)) {
+            m_window.close();
+        }
+
+        player.update(time, m_window, camera, world);
+        clouds.update(time);
+
+        GFX::Actions actions;
+
+        for (auto chunk : world.get_visible_chunks(time, camera.position())) {
+            auto position = chunk->chunk.value().position();
+            actions.add({
+                .program = GFX::Resources::Program::Terrain,
+                .mesh = &chunk->land_mesh.value(),
+                .transform = Transform(position),
+            });
+
+            actions.add({
+                .program = GFX::Resources::Program::Terrain,
+                .mesh = &chunk->water_mesh.value(),
+                .transform = Transform(position - Vec3{0, 0.2, 0}),
+                .alpha = 0.4,
+            });
+        }
+
+        player.render(actions);
+        clouds.render(actions, player.position());
+
+        m_render_control->send_render_data({actions, camera});
+        m_render_control->wait_for_render_finish();
+
+        time.end_frame();
+    }
+}
+
+void Game::fix_macos_render() const {
+    static Bool moved = false;
+
+    if(!moved) {
+        I32 x, y;
+        glfwGetWindowPos(m_window.get(), &x, &y);
+        glfwSetWindowPos(m_window.get(), ++x, y);
+
+        moved = true;
+    }
+}
+
+}
\ No newline at end of file
diff --git a/src/Game.hpp b/src/Game.hpp
new file mode 100644
index 0000000..ccb0013
--- /dev/null
+++ b/src/Game.hpp
@@ -0,0 +1,20 @@
+#pragma once
+#include "Render.hpp"
+
+namespace MC {
+
+class Game {
+public:
+    explicit Game(GFX::Window& window, std::shared_ptr<Render::Control> control)
+        : m_window(window)
+        , m_render_control(std::move(control)) {}
+
+    void run() const;
+private:
+    void fix_macos_render() const;
+
+    GFX::Window& m_window;
+    std::shared_ptr<Render::Control> m_render_control;
+};
+
+}
diff --git a/src/Math/Rotation.hpp b/src/Math/Rotation.hpp
index d12ac29..d8dff72 100644
--- a/src/Math/Rotation.hpp
+++ b/src/Math/Rotation.hpp
@@ -15,6 +15,10 @@ struct Rotation {
         vector = wrap({pitch, yaw, roll });
     }
 
+    static Rotation zero() {
+        return {0, 0, 0};
+    }
+
     Vector<3> radians() const {
         return vector.map([](auto a) { return Math::radians(a); });
     }
diff --git a/src/Render.cpp b/src/Render.cpp
new file mode 100644
index 0000000..3955f1c
--- /dev/null
+++ b/src/Render.cpp
@@ -0,0 +1,102 @@
+#include "Render.hpp"
+#include "Assets.hpp"
+#include "Defines.hpp"
+#include "Time.hpp"
+#include "Common/Assert.hpp"
+#include "GFX/Image/PPMParser.hpp"
+#include "GFX/Shading/Program.hpp"
+#include "Math/MVP.hpp"
+
+namespace MC {
+
+void Render::run() {
+    m_window.attach();
+    setup_gl();
+
+    m_resources.initialize();
+
+    glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
+    m_window.on_size_change([](GLFWwindow* _, I32 w, I32 h) {
+        glViewport(0, 0, w, h);
+    });
+
+    auto image = GFX::Image::PPMParser(Assets::Images::atlas).parse();
+    auto texture = GFX::Texture(image);
+
+    glEnable(GL_DEPTH_TEST);
+    glDepthFunc(GL_LEQUAL);
+
+    glEnable(GL_BLEND);
+    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+    glEnable(GL_CULL_FACE);
+    glFrontFace(GL_CCW);
+    glCullFace(GL_BACK);
+
+    while (!m_window.should_close()) {
+        Scene scene = m_control->wait_for_render_data();
+
+        m_window.start_render();
+        render_scene(scene, texture);
+
+        m_control->finish_render();
+    }
+}
+
+void Render::render_scene(Scene const& scene, GFX::Texture const& texture) const {
+    Vector<3, F32> sky_color = SKY_COLOR;
+    glClearColor(sky_color.x(), sky_color.y(), sky_color.z(), 1.0f);
+    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+    auto view = Math::MVP::view<F32>(scene.camera.position(), scene.camera.angles());
+    auto projection = Math::MVP::perspective_projection<F32>(ASPECT, FOV, 0.1f, 1000.0f);
+
+    for (auto& action : scene.actions.actions()) {
+        auto& program = m_resources.program(action.program);
+        program.bind();
+
+        auto model_uniform = program.uniform("model_matrix");
+        auto view_uniform = program.uniform("view_matrix");
+        auto projection_uniform = program.uniform("projection_matrix");
+        auto sun_direction_uniform = program.uniform("sun_direction");
+        auto sky_color_uniform = program.uniform("sky_color");
+        auto mesh_alpha_uniform = program.uniform("mesh_alpha");
+
+        ASSERT(model_uniform.has_value() && view_uniform.has_value() && projection_uniform.has_value(),
+            "Program does not have the necessary MVP uniforms");
+
+        auto transform = action.transform;
+        auto model = Math::MVP::model<F32>(transform.position(), transform.scale(), transform.rotation());
+
+        model_uniform->set(model);
+        view_uniform->set(view);
+        projection_uniform->set(projection);
+
+        if (sun_direction_uniform.has_value())
+            sun_direction_uniform->set(SUN_DIRECTION);
+
+        if (sky_color_uniform.has_value())
+            sky_color_uniform->set(SKY_COLOR);
+
+        if (mesh_alpha_uniform.has_value())
+            mesh_alpha_uniform->set(action.alpha);
+
+        texture.bind();
+        action.mesh->bind();
+        glDrawElements(TO(GLenum, action.draw_mode), action.mesh->size(), GL_UNSIGNED_INT, nullptr);
+        action.mesh->unbind();
+        texture.unbind();
+
+        program.unbind();
+    }
+}
+
+void Render::setup_gl() {
+    GLenum error = glewInit();
+    if (error != GLEW_OK) {
+        std::string error_string(reinterpret_cast<Char const*>(glewGetErrorString(error)));
+        throw std::runtime_error("Failed to load GL functions: " + error_string);
+    }
+}
+
+}
diff --git a/src/Render.hpp b/src/Render.hpp
new file mode 100644
index 0000000..6b7cb65
--- /dev/null
+++ b/src/Render.hpp
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <condition_variable>
+#include "Defines.hpp"
+#include "Common/Sizes.hpp"
+#include "GFX/Actions.hpp"
+#include "GFX/Camera.hpp"
+#include "GFX/Texture.hpp"
+#include "GFX/Window.hpp"
+
+namespace MC {
+
+class Render {
+public:
+    struct Scene {
+        GFX::Actions actions;
+        GFX::Camera camera;
+    };
+
+    struct Control {
+        std::mutex mutex;
+        std::condition_variable cv;
+        Bool logic_done = false;
+        Bool render_done = false;
+
+        Scene scene;
+
+        void send_render_data(Scene new_scene) {
+            {
+                std::scoped_lock lock(mutex);
+                scene = std::move(new_scene);
+                logic_done = true;
+            }
+            cv.notify_one();
+        }
+
+        Scene wait_for_render_data() {
+            Scene new_scene;
+            {
+                std::unique_lock lock(mutex);
+                cv.wait(lock, [&]{
+                    return logic_done;
+                });
+                new_scene = scene;
+                logic_done = false;
+                render_done = false;
+            }
+            cv.notify_one();
+            return new_scene;
+        }
+
+        void wait_for_render_finish() {
+            {
+                std::unique_lock lock(mutex);
+                cv.wait(lock, [&]{
+                    return render_done;
+                });
+                logic_done = false;
+                render_done = false;
+            }
+            cv.notify_one();
+        }
+
+        void finish_render() {
+            {
+                std::scoped_lock lock(mutex);
+                render_done = true;
+            }
+            cv.notify_one();
+        }
+    };
+
+    explicit Render(GFX::Window& window, std::shared_ptr<Control> control)
+        : m_window(window)
+        , m_control(std::move(control)) {}
+
+    void run();
+
+private:
+    void render_scene(Scene const& actions, GFX::Texture const& texture) const;
+    static void setup_gl();
+
+    GFX::Resources m_resources;
+    GFX::Window& m_window;
+    std::shared_ptr<Control> m_control;
+};
+
+}
diff --git a/src/Util/ImageViewer.cpp b/src/Util/ImageViewer.cpp
index 4c78750..05d316c 100644
--- a/src/Util/ImageViewer.cpp
+++ b/src/Util/ImageViewer.cpp
@@ -1,38 +1,15 @@
-#include <GL/glew.h>
 #include "ImageViewer.hpp"
-#include "../Math/MVP.hpp"
 
 namespace MC::Util {
 
-ImageViewer::ImageViewer(
-        const GFX::Image::RawImage& image,
-        Real window_aspect
-) : m_texture(image),
-    m_program(
-        {GFX::Shading::Shader::Type::Vertex, vertex},
-        {GFX::Shading::Shader::Type::Fragment, fragment}
-    ),
-    m_mesh(create_mesh(window_aspect, image.width(), image.height())) {
-    m_program.bind();
-    auto model_uniform = m_program.uniform("model_matrix");
-    auto view_uniform = m_program.uniform("view_matrix");
-    auto projection_uniform = m_program.uniform("projection_matrix");
-
-    model_uniform.set(Math::MVP::model<F32>({}, Vector<3>::one(), {}));
-    view_uniform.set(Math::MVP::view<F32>({}, {}));
-    projection_uniform.set(Math::MVP::orthographic_projection<F32>(view_size * window_aspect, view_size, 0.0f, 100.0f));
-
-    m_program.unbind();
-}
-
-void ImageViewer::render() {
-    m_program.bind();
-    m_texture.bind();
-    m_mesh.bind();
-    glDrawElements(GL_TRIANGLES, m_mesh.size(), GL_UNSIGNED_INT, nullptr);
-    m_mesh.unbind();
-    m_texture.unbind();
-    m_program.unbind();
+void ImageViewer::render(GFX::Actions& actions) {
+    // TODO: Re-add texture support
+    // TODO: Add orthographic camera support
+    actions.add({
+        .program = GFX::Resources::Program::ImageViewer,
+        .mesh = &m_mesh,
+        .transform = Transform(),
+    });
 }
 
 GFX::Mesh ImageViewer::create_mesh(Real window_aspect, U32 image_width, U32 image_height) {
@@ -60,33 +37,4 @@ GFX::Mesh ImageViewer::create_mesh(Real window_aspect, U32 image_width, U32 imag
     }, {0, 1, 2, 0, 2, 3}};
 }
 
-const Char* ImageViewer::vertex = R"v(
-#version 330 core
-
-uniform mat4 model_matrix;
-uniform mat4 view_matrix;
-uniform mat4 projection_matrix;
-
-layout (location = 0) in vec3 position;
-layout (location = 1) in vec2 tex_coord;
-
-out vec2 frag_tex_coord;
-
-void main() {
-    gl_Position = projection_matrix * view_matrix * model_matrix * vec4(position, 1.0);
-    frag_tex_coord = tex_coord;
-})v";
-
-const Char* ImageViewer::fragment = R"f(
-#version 330 core
-
-uniform sampler2D image;
-
-in vec2 frag_tex_coord;
-out vec4 color;
-
-void main() {
-    color = texture(image, frag_tex_coord);
-})f";
-
 }
\ No newline at end of file
diff --git a/src/Util/ImageViewer.hpp b/src/Util/ImageViewer.hpp
index aa48b1e..cfe9d18 100644
--- a/src/Util/ImageViewer.hpp
+++ b/src/Util/ImageViewer.hpp
@@ -1,27 +1,25 @@
 #pragma once
 
+#include "../GFX/Actions.hpp"
 #include "../GFX/Mesh.hpp"
 #include "../GFX/Image/RawImage.hpp"
 #include "../GFX/Texture.hpp"
-#include "../GFX/Shading/Program.hpp"
 
 namespace MC::Util {
 
 class ImageViewer {
 public:
-    explicit ImageViewer(const GFX::Image::RawImage& image, Real window_aspect);
+    explicit ImageViewer(const GFX::Image::RawImage& image, Real window_aspect)
+        : m_mesh(create_mesh(window_aspect, image.width(), image.height())),
+          m_texture(image) {}
 
-    void render();
+    void render(GFX::Actions& actions);
 private:
     static GFX::Mesh create_mesh(Real window_aspect, U32 image_width, U32 image_height);
 
     static constexpr Real view_size = 1000.0f;
 
-    static const Char* vertex;
-    static const Char* fragment;
-
     GFX::Mesh m_mesh;
-    GFX::Shading::Program m_program;
     GFX::Texture m_texture;
 };
 
diff --git a/src/World/Clouds.cpp b/src/World/Clouds.cpp
index b30c0ce..58b1919 100644
--- a/src/World/Clouds.cpp
+++ b/src/World/Clouds.cpp
@@ -5,65 +5,47 @@
 #include "../Math/AABB.hpp"
 #include "../GFX/Util/Primitives.hpp"
 #include "../GFX/Util/MeshBuilder.hpp"
-#include <GL/glew.h>
 #include <array>
 
 namespace MC::World {
 
-Clouds::Clouds(Real ascept, Real fov, Real near, Real far, Vector<3, F32> sky_color, Vector<3, F32> sun_direction)
-    : m_program(
-    {GFX::Shading::Shader::Type::Vertex, vertex},
-    {GFX::Shading::Shader::Type::Fragment, fragment}
-    ),
-    m_mesh(create_mesh(create_cloud_matrix())),
-    m_model_uniform(), m_view_uniform(), m_projection_uniform() {
-    m_program.bind();
-
-    m_model_uniform = m_program.uniform("model_matrix");
-    m_view_uniform = m_program.uniform("view_matrix");
-    m_projection_uniform = m_program.uniform("projection_matrix");
-    auto sky_color_uniform = m_program.uniform("sky_color");
-    auto sun_direction_uniform = m_program.uniform("sun_direction");
-
-    m_model_uniform.set(Math::MVP::model<F32>({}, {}, {}));
-    m_view_uniform.set(Math::MVP::view<F32>({}, {}));
-    m_projection_uniform.set(Math::MVP::perspective_projection<F32>(ascept, fov, near, far));
-    sky_color_uniform.set(sky_color);
-    sun_direction_uniform.set(sun_direction);
-
-    m_program.unbind();
-}
-
-void Clouds::update(const Time& time) {
+void Clouds::update(Time const& time) {
     m_x_offset += 5.0 * time.delta();
 }
 
-void Clouds::render(const GFX::Camera& camera) {
-    auto position = camera.position();
-
-    I32 center_x = std::round((position.x() - m_x_offset) / TileSize);
-    I32 center_y = std::round(position.z() / TileSize);
+void Clouds::render(GFX::Actions& actions, Position::World player_position) {
+    I32 center_x = std::round((player_position.x() - m_x_offset) / TileSize);
+    I32 center_y = std::round(player_position.z() / TileSize);
 
     for (Int x = -1; x <= 1; x++) {
         for (Int y = -1; y <= 1; y++) {
-            render_single_instance(camera, center_x + x, center_y + y);
+            render_single_instance(actions, center_x + x, center_y + y);
         }
     }
 }
 
-void Clouds::render_single_instance(const GFX::Camera& camera, Int x, Int y) {
+void Clouds::render_single_instance(GFX::Actions& actions, Int x, Int y) {
     Vector<3> position{TileSize * x + m_x_offset, Height, TileSize * y};
 
-    m_program.bind();
-
-    m_view_uniform.set(Math::MVP::view<F32>(camera.position(), camera.angles()));
-    m_model_uniform.set(Math::MVP::model<F32>(position, Vector<3>{Scale}, {}));
-
-    m_mesh.bind();
-    glDrawElements(GL_TRIANGLES, m_mesh.size(), GL_UNSIGNED_INT, nullptr);
-    m_mesh.unbind();
-
-    m_program.unbind();
+    // m_program.bind();
+    //
+    // m_view_uniform.set(Math::MVP::view<F32>(camera.position(), camera.angles()));
+    // m_model_uniform.set(Math::MVP::model<F32>(position, Vector<3>{Scale}, {}));
+    //
+    // m_mesh.bind();
+    // glDrawElements(GL_TRIANGLES, m_mesh.size(), GL_UNSIGNED_INT, nullptr);
+    // m_mesh.unbind();
+    //
+    // m_program.unbind();
+    actions.add({
+        .mesh = &m_mesh,
+        .program = GFX::Resources::Program::Clouds,
+        .transform = Transform(
+            position,
+            Rotation::zero(),
+            Vec3(Scale)
+        ),
+    });
 }
 
 Clouds::CloudMatrix Clouds::create_cloud_matrix() {
@@ -80,7 +62,7 @@ Clouds::CloudMatrix Clouds::create_cloud_matrix() {
     return clouds;
 }
 
-GFX::Mesh Clouds::create_mesh(const CloudMatrix& cloud_matrix) {
+GFX::Mesh Clouds::create_mesh(CloudMatrix const& cloud_matrix) {
     GFX::Util::MeshBuilder<Vector<3, F32>> builder{};
 
     for (Int x = 0; x < CloudMatrixSize; x++) {
@@ -112,47 +94,4 @@ GFX::Mesh Clouds::create_mesh(const CloudMatrix& cloud_matrix) {
     return builder.mesh();
 }
 
-const Char* Clouds::vertex = R"v(
-#version 330 core
-
-uniform mat4 model_matrix;
-uniform mat4 view_matrix;
-uniform mat4 projection_matrix;
-
-layout (location = 0) in vec3 position;
-layout (location = 1) in vec3 normal;
-
-out vec3 surface_normal;
-out float depth;
-
-void main() {
-    vec4 world_position = model_matrix * vec4(position, 1.0);
-    vec4 view_position = view_matrix * world_position;
-    vec4 clip_position = projection_matrix * view_position;
-
-    gl_Position = clip_position;
-    surface_normal = (model_matrix * vec4(normal, 0.0)).xyz;
-    depth = clamp((length(view_position) - 200) / 400, 0.0, 1.0);
-}
-)v";
-
-const Char* Clouds::fragment = R"f(
-#version 330 core
-
-uniform vec3 sky_color;
-uniform vec3 sun_direction;
-
-in vec3 surface_normal;
-in float depth;
-out vec4 color;
-
-void main() {
-    float brightness = dot(normalize(surface_normal), normalize(-sun_direction));
-    vec3 diffuse = vec3(1 - clamp(brightness, -0.3, 0.2));
-    vec3 base = vec3(1.0, 1.0, 1.0) * diffuse;
-
-    color = vec4(mix(sky_color, base, 1 - depth), 0.3);
-}
-)f";
-
 }
diff --git a/src/World/Clouds.hpp b/src/World/Clouds.hpp
index 4bd0637..7246e7b 100644
--- a/src/World/Clouds.hpp
+++ b/src/World/Clouds.hpp
@@ -1,8 +1,7 @@
 #pragma once
 
 #include "../Time.hpp"
-#include "../GFX/Shading/Program.hpp"
-#include "../GFX/Shading/Uniform.hpp"
+#include "../GFX/Actions.hpp"
 #include "../GFX/Camera.hpp"
 #include "../GFX/Mesh.hpp"
 
@@ -10,10 +9,10 @@ namespace MC::World {
 
 class Clouds {
 public:
-    Clouds(Real ascept, Real fov, Real near, Real far, Vector<3, F32> sky_color, Vector<3, F32> sun_direction);
+    Clouds() : m_mesh(create_mesh(create_cloud_matrix())) {}
 
-    void update(const Time& time);
-    void render(const GFX::Camera& camera);
+    void update(Time const& time);
+    void render(GFX::Actions& actions, Position::World player_position);
 private:
     constexpr static U32 CloudMatrixSize = 128;
     constexpr static Int Height = 200;
@@ -22,22 +21,13 @@ private:
 
     using CloudMatrix = Matrix<CloudMatrixSize, CloudMatrixSize, Bool>;
 
-    void render_single_instance(const GFX::Camera& camera, Int x, Int y);
+    void render_single_instance(GFX::Actions& actions, Int x, Int y);
 
     static CloudMatrix create_cloud_matrix();
-    static GFX::Mesh create_mesh(const CloudMatrix& cloud_matrix);
-
-    static const Char* vertex;
-    static const Char* fragment;
+    static GFX::Mesh create_mesh(CloudMatrix const& cloud_matrix);
 
     Real m_x_offset = 0.0;
-
-    GFX::Shading::Program m_program;
     GFX::Mesh m_mesh;
-
-    GFX::Shading::Uniform m_model_uniform;
-    GFX::Shading::Uniform m_view_uniform;
-    GFX::Shading::Uniform m_projection_uniform;
 };
 
 }
diff --git a/src/main.cpp b/src/main.cpp
index 9dd42c9..9153530 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -2,36 +2,30 @@
 #include <GL/glew.h>
 #include <GLFW/glfw3.h>
 
-#include "Time.hpp"
-#include "Common/Sizes.hpp"
-#include "GFX/Window.hpp"
-#include "GFX/Camera.hpp"
-#include "Math/MVP.hpp"
-#include "GFX/Shading/Program.hpp"
-#include "GFX/Texture.hpp"
-#include "GFX/Image/PPMParser.hpp"
-#include "World/Clouds.hpp"
+#include "Game.hpp"
+#include "Render.hpp"
 #include "World/World.hpp"
-#include "Entities/Player.hpp"
 
-#define APP_NAME "Meowcraft"
-
-#define WINDOW_WIDTH 800
-#define WINDOW_HEIGHT 600
-#define ASPECT (static_cast<Real>(WINDOW_WIDTH) / WINDOW_HEIGHT)
+int main() {
+    if (!glfwInit()) {
+        std::cout << "Failed to initialize GLFW" << std::endl;
+        return 1;
+    }
 
-#define FOV 90
+    try {
+        MC::GFX::Window window(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT);
+        window.detach();
 
-void run();
-void render(MC::GFX::Mesh&, MC::GFX::Texture&);
-void setup_gl();
-void fix_macos_render(const MC::GFX::Window&);
+        auto render_control = std::make_shared<MC::Render::Control>();
 
-int main() {
-    glfwInit();
+        std::thread render_thread{[&window, render_control] {
+            MC::Render render{window, render_control};
+            render.run();
+        }};
+        render_thread.detach();
 
-    try {
-        run();
+        MC::Game game{window, render_control};
+        game.run();
     } catch (std::runtime_error& error) {
         std::cout << "An error occurred: " << error.what() << std::endl;
         glfwTerminate();
@@ -41,128 +35,3 @@ int main() {
     glfwTerminate();
     return 0;
 }
-
-void run() {
-    MC::GFX::Window window(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT);
-    setup_gl();
-
-    glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
-    window.on_size_change([](GLFWwindow* _, I32 w, I32 h) {
-        glViewport(0, 0, w, h);
-    });
-
-    auto image = MC::GFX::Image::PPMParser(MC::Assets::Images::atlas).parse();
-    auto texture = MC::GFX::Texture(image);
-
-    MC::World::World world;
-
-    MC::GFX::Camera camera{};
-
-    MC::GFX::Shading::Program program(
-        MC::GFX::Shading::Shader::create_vertex(),
-        MC::GFX::Shading::Shader::create_fragment()
-    );
-
-    auto model_uniform = program.uniform("model_matrix");
-    auto view_uniform = program.uniform("view_matrix");
-    auto projection_uniform = program.uniform("projection_matrix");
-    auto sun_direction_uniform = program.uniform("sun_direction");
-    auto sky_color_uniform = program.uniform("sky_color");
-    auto mesh_alpha_uniform = program.uniform("mesh_alpha");
-
-    program.bind();
-    auto projection = Math::MVP::perspective_projection<F32>(ASPECT, FOV, 0.1f, 1000.0f);
-    projection_uniform.set(projection);
-
-    Vector<3, F32> sun_direction{1, -1, 0};
-    sun_direction_uniform.set(sun_direction);
-
-    Vector<3, F32> sky_color{0.85, 0.85, 0.85}; // #DBDBDB
-    sky_color_uniform.set(sky_color);
-
-    MC::World::Clouds clouds{ASPECT, FOV, 0.1f, 1000.0f, sky_color, sun_direction};
-
-    MC::Entities::Player player{{0, MC::World::Chunk::Height / 2.0, 0}};
-
-    glEnable(GL_DEPTH_TEST);
-    glDepthFunc(GL_LEQUAL);
-
-    glEnable(GL_BLEND);
-    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
-    glEnable(GL_CULL_FACE);
-    glFrontFace(GL_CCW);
-    glCullFace(GL_BACK);
-
-    MC::Time time;
-
-    while (!window.should_close()) {
-        time.start_frame();
-        window.start_frame();
-
-#ifdef __APPLE__
-        fix_macos_render(window);
-#endif
-
-        if (window.key(GLFW_KEY_ESCAPE, GLFW_PRESS)) {
-            window.close();
-        }
-
-        player.update(time, window, camera, world);
-        clouds.update(time);
-
-        glClearColor(sky_color.x(), sky_color.y(), sky_color.z(), 1.0f); // #DBDBDB
-        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
-        program.bind();
-
-        auto view = Math::MVP::view<F32>(camera.position(), camera.angles());
-        view_uniform.set(view);
-
-        for (auto chunk : world.get_visible_chunks(camera.position())) {
-            mesh_alpha_uniform.set(1.0);
-            auto land_model = Math::MVP::model<F32>(chunk->chunk.value().position(), Vector<3>::one(), {});
-            model_uniform.set(land_model);
-            render(chunk->land_mesh.value(), texture);
-
-            mesh_alpha_uniform.set(0.4);
-            auto water_model = Math::MVP::model<F32>(chunk->chunk.value().position() - Vector<3>{0, 0.2, 0}, Vector<3>::one(), {});
-            model_uniform.set(water_model);
-            render(chunk->water_mesh.value(), texture);
-        }
-
-        program.unbind();
-
-        clouds.render(camera);
-
-        time.end_frame();
-    }
-}
-
-void render(MC::GFX::Mesh& mesh, MC::GFX::Texture& texture) {
-    texture.bind();
-    mesh.bind();
-    glDrawElements(GL_TRIANGLES, mesh.size(), GL_UNSIGNED_INT, nullptr);
-    mesh.unbind();
-    texture.unbind();
-}
-
-void setup_gl() {
-    GLenum error;
-    if ((error = glewInit()) != GLEW_OK) {
-        std::string error_string(reinterpret_cast<const Char*>(glewGetErrorString(error)));
-        throw std::runtime_error("Failed to load GL functions: " + error_string);
-    }
-}
-
-void fix_macos_render(const MC::GFX::Window& window) {
-    static Bool moved = false;
-
-    if(!moved) {
-        I32 x, y;
-        glfwGetWindowPos(window.get(), &x, &y);
-        glfwSetWindowPos(window.get(), ++x, y);
-
-        moved = true;
-    }
-}
\ No newline at end of file