From 039d96eee340278734601d1c7bc3f7b0010f39f4 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 17 Feb 2026 22:17:19 +0100 Subject: [PATCH] replace manual 3d-2d projection with orbital camera --- include/config.hpp | 4 +- include/renderer.hpp | 63 +++++++++++-------- src/main.cpp | 30 ++------- src/mass_springs.cpp | 6 ++ src/renderer.cpp | 142 ++++++++++++++----------------------------- 5 files changed, 97 insertions(+), 148 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 91d439e..1909a24 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -6,7 +6,7 @@ constexpr int WIDTH = 800; constexpr int HEIGHT = 800; -constexpr float VERTEX_SIZE = 50.0; +constexpr float VERTEX_SIZE = 0.1; constexpr Color VERTEX_COLOR = GREEN; constexpr Color EDGE_COLOR = DARKGREEN; @@ -18,7 +18,7 @@ constexpr float CULLING_TOLERANCE = 0.1; // percentage constexpr float DEFAULT_SPRING_CONSTANT = 1.5; constexpr float DEFAULT_DAMPENING_CONSTANT = 0.1; constexpr float DEFAULT_REST_LENGTH = 0.5; -constexpr float DEFAULT_REPULSION_FORCE = 0.002; +constexpr float DEFAULT_REPULSION_FORCE = 0.01; constexpr int BOARD_PADDING = 5; constexpr int BLOCK_PADDING = 5; diff --git a/include/renderer.hpp b/include/renderer.hpp index ea42f96..6363f75 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -4,26 +4,52 @@ #include #include #include -#include +#include "config.hpp" #include "klotski.hpp" #include "mass_springs.hpp" -using Edge3Set = std::vector>; -using Edge2Set = std::vector>; -using Vertex2Set = - std::vector; // Vertex2Set uses Vector3 to retain the z-coordinate - // for circle size adaptation +class OrbitCamera3D { + friend class Renderer; + +private: + Camera camera; + Vector3 target; + float distance; + float angle_x; + float angle_y; + Vector2 last_mouse; + bool dragging; + +public: + OrbitCamera3D(Vector3 target, float distance) + : camera({0}), target(target), distance(distance), angle_x(0.0), + angle_y(0.3), last_mouse(Vector2Zero()), dragging(false) { + camera.position = Vector3(0, 0, -1.0 * distance); + camera.target = target; + camera.up = Vector3(0, 1.0, 0); + camera.fovy = 90.0; + camera.projection = CAMERA_PERSPECTIVE; + } + + ~OrbitCamera3D() {} + +public: + auto Update() -> void; +}; class Renderer { private: int width; int height; - RenderTexture2D render_target; - RenderTexture2D klotski_target; + OrbitCamera3D camera; + RenderTexture render_target; + RenderTexture klotski_target; public: - Renderer(int width, int height) : width(width), height(height) { + Renderer(int width, int height) + : width(width), height(height), + camera(OrbitCamera3D(Vector3(0, 0, 0), CAMERA_DISTANCE)) { render_target = LoadRenderTexture(width, height); klotski_target = LoadRenderTexture(width, height); } @@ -38,25 +64,10 @@ public: UnloadRenderTexture(klotski_target); } -private: - auto Rotate(const Vector3 &a, const float cos_angle, const float sin_angle) - -> Vector3; - - auto Translate(const Vector3 &a, const float distance, const float horizontal, - const float vertical) -> Vector3; - - auto Project(const Vector3 &a) -> Vector2; - - auto Map(const Vector2 &a) -> Vector2; - public: - auto Transform(Edge2Set &edges, Vertex2Set &vertices, - const MassSpringSystem &mass_springs, const float angle, - const float distance, const float horizontal, - const float vertical) -> void; + auto UpdateCamera() -> void; - auto DrawMassSprings(const Edge2Set &edges, const Vertex2Set &vertices) - -> void; + auto DrawMassSprings(const MassSpringSystem &masssprings) -> void; auto DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, int sel_y) -> void; diff --git a/src/main.cpp b/src/main.cpp index dac0284..512a054 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,9 +44,10 @@ auto main(int argc, char *argv[]) -> int { SetTraceLogLevel(LOG_ERROR); - // SetTargetFPS(60); + // SetTargetFPS(165); SetConfigFlags(FLAG_VSYNC_HINT); SetConfigFlags(FLAG_MSAA_4X_HINT); + // SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); InitWindow(WIDTH * 2, HEIGHT, "MassSprings"); @@ -59,17 +60,9 @@ auto main(int argc, char *argv[]) -> int { // Rendering configuration Renderer renderer(WIDTH, HEIGHT); - Edge2Set edges; - edges.reserve(mass_springs.springs.size()); - Vertex2Set vertices; - vertices.reserve(mass_springs.masses.size()); // Game loop - float camera_distance = CAMERA_DISTANCE; - float horizontal = 0.0; - float vertical = 0.0; float frametime; - float abstime = 0.0; int hov_x = 0; int hov_y = 0; int sel_x = 0; @@ -78,7 +71,6 @@ auto main(int argc, char *argv[]) -> int { frametime = GetFrameTime(); // Mouse handling - Vector2 m = GetMousePosition(); float block_size; float x_offset = 0.0; float y_offset = 0.0; @@ -89,6 +81,7 @@ auto main(int argc, char *argv[]) -> int { block_size = static_cast(HEIGHT) / board.height; x_offset = (WIDTH - block_size * board.width) / 2.0; } + Vector2 m = GetMousePosition(); if (m.x < x_offset) { hov_x = 100; } else { @@ -103,7 +96,6 @@ auto main(int argc, char *argv[]) -> int { sel_x = hov_x; sel_y = hov_y; } - camera_distance += GetMouseWheelMove() / -10.0; // Key handling std::string previous_state = board.state; @@ -136,15 +128,6 @@ auto main(int argc, char *argv[]) -> int { DEFAULT_SPRING_CONSTANT, DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH); } - if (IsKeyPressed(KEY_UP)) { - vertical += 0.1; - } else if (IsKeyPressed(KEY_RIGHT)) { - horizontal += 0.1; - } else if (IsKeyPressed(KEY_DOWN)) { - vertical -= 0.1; - } else if (IsKeyPressed(KEY_LEFT)) { - horizontal -= 0.1; - } // Physics update mass_springs.ClearForces(); @@ -157,13 +140,10 @@ auto main(int argc, char *argv[]) -> int { #endif // Rendering - renderer.Transform(edges, vertices, mass_springs, abstime * ROTATION_SPEED, - camera_distance, horizontal, vertical); - renderer.DrawMassSprings(edges, vertices); + renderer.DrawMassSprings(mass_springs); renderer.DrawKlotski(board, hov_x, hov_y, sel_x, sel_y); renderer.DrawTextures(); - - abstime += frametime; + renderer.UpdateCamera(); } CloseWindow(); diff --git a/src/mass_springs.cpp b/src/mass_springs.cpp index 02e41df..469b6cd 100644 --- a/src/mass_springs.cpp +++ b/src/mass_springs.cpp @@ -114,6 +114,12 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void { for (auto &[state, mass] : masses) { for (auto &[s, m] : masses) { Vector3 dx = Vector3Subtract(mass.position, m.position); + + // This can be accelerated with a spatial data structure + if (Vector3Length(dx) >= 3 * DEFAULT_REST_LENGTH) { + continue; + } + mass.force = Vector3Add(mass.force, Vector3Scale(Vector3Normalize(dx), DEFAULT_REPULSION_FORCE)); diff --git a/src/renderer.cpp b/src/renderer.cpp index b9ebaba..224def5 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -3,118 +3,70 @@ #include #include "config.hpp" +#include "mass_springs.hpp" -auto Renderer::Rotate(const Vector3 &a, const float cos_angle, - const float sin_angle) -> Vector3 { - return Vector3(a.x * cos_angle - a.z * sin_angle, a.y, - a.x * sin_angle + a.z * cos_angle); -}; +auto OrbitCamera3D::Update() -> void { + Vector2 mouse = GetMousePosition(); -auto Renderer::Translate(const Vector3 &a, const float distance, - const float horizontal, const float vertical) - -> Vector3 { - return Vector3(a.x + horizontal, a.y + vertical, a.z + distance); -}; - -auto Renderer::Project(const Vector3 &a) -> Vector2 { - return Vector2(a.x / a.z, a.y / a.z); -} - -auto Renderer::Map(const Vector2 &a) -> Vector2 { - return Vector2((1.0 + a.x) / 2.0 * width, (1.0 - a.y) * height / 2.0); -} - -auto Renderer::Transform(Edge2Set &edges, Vertex2Set &vertices, - const MassSpringSystem &mass_springs, - const float angle, const float distance, - const float horizontal, const float vertical) -> void { - const float cos_angle = cos(angle); - const float sin_angle = sin(angle); - - edges.clear(); - for (const auto &spring : mass_springs.springs) { - const Mass &massA = spring.massA; - const Mass &massB = spring.massB; - - // Stuff behind the camera - if (massA.position.z + distance <= 0.1) { - continue; - } - if (massB.position.z + distance <= 0.1) { - continue; - } - - Vector2 a = - Map(Project(Translate(Rotate(massA.position, cos_angle, sin_angle), - distance, horizontal, vertical))); - Vector2 b = - Map(Project(Translate(Rotate(massB.position, cos_angle, sin_angle), - distance, horizontal, vertical))); - - // Stuff outside the viewport - if (!CheckCollisionPointRec( - a, Rectangle(-1.0 * width * CULLING_TOLERANCE, - -1.0 * height * CULLING_TOLERANCE, - width + width * CULLING_TOLERANCE * 2.0, - height + height * CULLING_TOLERANCE * 2.0))) { - continue; - } - if (!CheckCollisionPointRec( - b, Rectangle(-1.0 * width * CULLING_TOLERANCE, - -1.0 * height * CULLING_TOLERANCE, - width + width * CULLING_TOLERANCE * 2.0, - height + height * CULLING_TOLERANCE * 2.0))) { - continue; - } - - edges.emplace_back(a, b); + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + dragging = true; + last_mouse = mouse; } - // This is duplicated work, but easy to read - vertices.clear(); - for (const auto &[state, mass] : mass_springs.masses) { - - // Stuff behind the camera - if (mass.position.z + distance <= 0.1) { - continue; - } - - Vector3 a = Translate(Rotate(mass.position, cos_angle, sin_angle), distance, - horizontal, vertical); - Vector2 b = Map(Project(a)); - - // Stuff outside the viewport - if (!CheckCollisionPointRec( - b, Rectangle(-1.0 * width * CULLING_TOLERANCE, - -1.0 * height * CULLING_TOLERANCE, - width + width * CULLING_TOLERANCE * 2.0, - height + height * CULLING_TOLERANCE * 2.0))) { - continue; - } - - vertices.emplace_back(b.x, b.y, a.z); + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + dragging = false; } + + if (dragging) { + Vector2 dx = Vector2Subtract(mouse, last_mouse); + last_mouse = mouse; + + angle_x -= dx.x * 0.005; + angle_y += dx.y * 0.005; + + angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping + } + + float wheel = GetMouseWheelMove(); + distance -= wheel * 2.0; + distance = Clamp(distance, 2.0, 50.0); + + float x = cos(angle_y) * sin(angle_x) * distance; + float y = sin(angle_y) * distance; + float z = cos(angle_y) * cos(angle_x) * distance; + + camera.position = Vector3Add(target, Vector3(x, y, z)); + camera.target = target; } -auto Renderer::DrawMassSprings(const Edge2Set &edges, - const Vertex2Set &vertices) -> void { +auto Renderer::UpdateCamera() -> void { camera.Update(); } + +auto Renderer::DrawMassSprings(const MassSpringSystem &masssprings) -> void { BeginTextureMode(render_target); ClearBackground(RAYWHITE); + BeginMode3D(camera.camera); + // Draw springs - for (const auto &[a, b] : edges) { - DrawLine(a.x, a.y, b.x, b.y, EDGE_COLOR); + for (const auto &spring : masssprings.springs) { + const Mass a = spring.massA; + const Mass b = spring.massB; + + DrawLine3D(a.position, b.position, EDGE_COLOR); } // Draw masses - for (const auto &a : vertices) { - // Increase the perspective perception by squaring the z-coordinate - const float size = Clamp(VERTEX_SIZE / (a.z * a.z), 0.1, 100.0); - - DrawRectangle(a.x - size / 2.0, a.y - size / 2.0, size, size, VERTEX_COLOR); + for (const auto &[state, mass] : masssprings.masses) { + DrawCube(mass.position, VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE, + VERTEX_COLOR); } + DrawGrid(10, 1.0); + + EndMode3D(); + DrawLine(0, 0, 0, height, BLACK); + EndTextureMode(); }