replace manual 3d-2d projection with orbital camera

This commit is contained in:
2026-02-17 22:17:19 +01:00
parent 8d5a6a827c
commit 039d96eee3
5 changed files with 97 additions and 148 deletions

View File

@ -6,7 +6,7 @@
constexpr int WIDTH = 800; constexpr int WIDTH = 800;
constexpr int HEIGHT = 800; constexpr int HEIGHT = 800;
constexpr float VERTEX_SIZE = 50.0; constexpr float VERTEX_SIZE = 0.1;
constexpr Color VERTEX_COLOR = GREEN; constexpr Color VERTEX_COLOR = GREEN;
constexpr Color EDGE_COLOR = DARKGREEN; 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_SPRING_CONSTANT = 1.5;
constexpr float DEFAULT_DAMPENING_CONSTANT = 0.1; constexpr float DEFAULT_DAMPENING_CONSTANT = 0.1;
constexpr float DEFAULT_REST_LENGTH = 0.5; 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 BOARD_PADDING = 5;
constexpr int BLOCK_PADDING = 5; constexpr int BLOCK_PADDING = 5;

View File

@ -4,26 +4,52 @@
#include <immintrin.h> #include <immintrin.h>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <vector>
#include "config.hpp"
#include "klotski.hpp" #include "klotski.hpp"
#include "mass_springs.hpp" #include "mass_springs.hpp"
using Edge3Set = std::vector<std::pair<Vector3, Vector3>>; class OrbitCamera3D {
using Edge2Set = std::vector<std::pair<Vector2, Vector2>>; friend class Renderer;
using Vertex2Set =
std::vector<Vector3>; // Vertex2Set uses Vector3 to retain the z-coordinate private:
// for circle size adaptation 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 { class Renderer {
private: private:
int width; int width;
int height; int height;
RenderTexture2D render_target; OrbitCamera3D camera;
RenderTexture2D klotski_target; RenderTexture render_target;
RenderTexture klotski_target;
public: 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); render_target = LoadRenderTexture(width, height);
klotski_target = LoadRenderTexture(width, height); klotski_target = LoadRenderTexture(width, height);
} }
@ -38,25 +64,10 @@ public:
UnloadRenderTexture(klotski_target); 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: public:
auto Transform(Edge2Set &edges, Vertex2Set &vertices, auto UpdateCamera() -> void;
const MassSpringSystem &mass_springs, const float angle,
const float distance, const float horizontal,
const float vertical) -> void;
auto DrawMassSprings(const Edge2Set &edges, const Vertex2Set &vertices) auto DrawMassSprings(const MassSpringSystem &masssprings) -> void;
-> void;
auto DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, int sel_y) auto DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, int sel_y)
-> void; -> void;

View File

@ -44,9 +44,10 @@ auto main(int argc, char *argv[]) -> int {
SetTraceLogLevel(LOG_ERROR); SetTraceLogLevel(LOG_ERROR);
// SetTargetFPS(60); // SetTargetFPS(165);
SetConfigFlags(FLAG_VSYNC_HINT); SetConfigFlags(FLAG_VSYNC_HINT);
SetConfigFlags(FLAG_MSAA_4X_HINT); SetConfigFlags(FLAG_MSAA_4X_HINT);
// SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN);
InitWindow(WIDTH * 2, HEIGHT, "MassSprings"); InitWindow(WIDTH * 2, HEIGHT, "MassSprings");
@ -59,17 +60,9 @@ auto main(int argc, char *argv[]) -> int {
// Rendering configuration // Rendering configuration
Renderer renderer(WIDTH, HEIGHT); Renderer renderer(WIDTH, HEIGHT);
Edge2Set edges;
edges.reserve(mass_springs.springs.size());
Vertex2Set vertices;
vertices.reserve(mass_springs.masses.size());
// Game loop // Game loop
float camera_distance = CAMERA_DISTANCE;
float horizontal = 0.0;
float vertical = 0.0;
float frametime; float frametime;
float abstime = 0.0;
int hov_x = 0; int hov_x = 0;
int hov_y = 0; int hov_y = 0;
int sel_x = 0; int sel_x = 0;
@ -78,7 +71,6 @@ auto main(int argc, char *argv[]) -> int {
frametime = GetFrameTime(); frametime = GetFrameTime();
// Mouse handling // Mouse handling
Vector2 m = GetMousePosition();
float block_size; float block_size;
float x_offset = 0.0; float x_offset = 0.0;
float y_offset = 0.0; float y_offset = 0.0;
@ -89,6 +81,7 @@ auto main(int argc, char *argv[]) -> int {
block_size = static_cast<float>(HEIGHT) / board.height; block_size = static_cast<float>(HEIGHT) / board.height;
x_offset = (WIDTH - block_size * board.width) / 2.0; x_offset = (WIDTH - block_size * board.width) / 2.0;
} }
Vector2 m = GetMousePosition();
if (m.x < x_offset) { if (m.x < x_offset) {
hov_x = 100; hov_x = 100;
} else { } else {
@ -103,7 +96,6 @@ auto main(int argc, char *argv[]) -> int {
sel_x = hov_x; sel_x = hov_x;
sel_y = hov_y; sel_y = hov_y;
} }
camera_distance += GetMouseWheelMove() / -10.0;
// Key handling // Key handling
std::string previous_state = board.state; std::string previous_state = board.state;
@ -136,15 +128,6 @@ auto main(int argc, char *argv[]) -> int {
DEFAULT_SPRING_CONSTANT, DEFAULT_SPRING_CONSTANT,
DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH); 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 // Physics update
mass_springs.ClearForces(); mass_springs.ClearForces();
@ -157,13 +140,10 @@ auto main(int argc, char *argv[]) -> int {
#endif #endif
// Rendering // Rendering
renderer.Transform(edges, vertices, mass_springs, abstime * ROTATION_SPEED, renderer.DrawMassSprings(mass_springs);
camera_distance, horizontal, vertical);
renderer.DrawMassSprings(edges, vertices);
renderer.DrawKlotski(board, hov_x, hov_y, sel_x, sel_y); renderer.DrawKlotski(board, hov_x, hov_y, sel_x, sel_y);
renderer.DrawTextures(); renderer.DrawTextures();
renderer.UpdateCamera();
abstime += frametime;
} }
CloseWindow(); CloseWindow();

View File

@ -114,6 +114,12 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void {
for (auto &[state, mass] : masses) { for (auto &[state, mass] : masses) {
for (auto &[s, m] : masses) { for (auto &[s, m] : masses) {
Vector3 dx = Vector3Subtract(mass.position, m.position); 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 = mass.force =
Vector3Add(mass.force, Vector3Scale(Vector3Normalize(dx), Vector3Add(mass.force, Vector3Scale(Vector3Normalize(dx),
DEFAULT_REPULSION_FORCE)); DEFAULT_REPULSION_FORCE));

View File

@ -3,118 +3,70 @@
#include <raylib.h> #include <raylib.h>
#include "config.hpp" #include "config.hpp"
#include "mass_springs.hpp"
auto Renderer::Rotate(const Vector3 &a, const float cos_angle, auto OrbitCamera3D::Update() -> void {
const float sin_angle) -> Vector3 { Vector2 mouse = GetMousePosition();
return Vector3(a.x * cos_angle - a.z * sin_angle, a.y,
a.x * sin_angle + a.z * cos_angle);
};
auto Renderer::Translate(const Vector3 &a, const float distance, if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
const float horizontal, const float vertical) dragging = true;
-> Vector3 { last_mouse = mouse;
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 { if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
return Vector2((1.0 + a.x) / 2.0 * width, (1.0 - a.y) * height / 2.0); dragging = false;
} }
auto Renderer::Transform(Edge2Set &edges, Vertex2Set &vertices, if (dragging) {
const MassSpringSystem &mass_springs, Vector2 dx = Vector2Subtract(mouse, last_mouse);
const float angle, const float distance, last_mouse = mouse;
const float horizontal, const float vertical) -> void {
const float cos_angle = cos(angle);
const float sin_angle = sin(angle);
edges.clear(); angle_x -= dx.x * 0.005;
for (const auto &spring : mass_springs.springs) { angle_y += dx.y * 0.005;
const Mass &massA = spring.massA;
const Mass &massB = spring.massB;
// Stuff behind the camera angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping
if (massA.position.z + distance <= 0.1) {
continue;
}
if (massB.position.z + distance <= 0.1) {
continue;
} }
Vector2 a = float wheel = GetMouseWheelMove();
Map(Project(Translate(Rotate(massA.position, cos_angle, sin_angle), distance -= wheel * 2.0;
distance, horizontal, vertical))); distance = Clamp(distance, 2.0, 50.0);
Vector2 b =
Map(Project(Translate(Rotate(massB.position, cos_angle, sin_angle),
distance, horizontal, vertical)));
// Stuff outside the viewport float x = cos(angle_y) * sin(angle_x) * distance;
if (!CheckCollisionPointRec( float y = sin(angle_y) * distance;
a, Rectangle(-1.0 * width * CULLING_TOLERANCE, float z = cos(angle_y) * cos(angle_x) * distance;
-1.0 * height * CULLING_TOLERANCE,
width + width * CULLING_TOLERANCE * 2.0, camera.position = Vector3Add(target, Vector3(x, y, z));
height + height * CULLING_TOLERANCE * 2.0))) { camera.target = target;
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); auto Renderer::UpdateCamera() -> void { camera.Update(); }
}
// This is duplicated work, but easy to read auto Renderer::DrawMassSprings(const MassSpringSystem &masssprings) -> void {
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);
}
}
auto Renderer::DrawMassSprings(const Edge2Set &edges,
const Vertex2Set &vertices) -> void {
BeginTextureMode(render_target); BeginTextureMode(render_target);
ClearBackground(RAYWHITE); ClearBackground(RAYWHITE);
BeginMode3D(camera.camera);
// Draw springs // Draw springs
for (const auto &[a, b] : edges) { for (const auto &spring : masssprings.springs) {
DrawLine(a.x, a.y, b.x, b.y, EDGE_COLOR); const Mass a = spring.massA;
const Mass b = spring.massB;
DrawLine3D(a.position, b.position, EDGE_COLOR);
} }
// Draw masses // Draw masses
for (const auto &a : vertices) { for (const auto &[state, mass] : masssprings.masses) {
// Increase the perspective perception by squaring the z-coordinate DrawCube(mass.position, VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE,
const float size = Clamp(VERTEX_SIZE / (a.z * a.z), 0.1, 100.0); VERTEX_COLOR);
DrawRectangle(a.x - size / 2.0, a.y - size / 2.0, size, size, VERTEX_COLOR);
} }
DrawGrid(10, 1.0);
EndMode3D();
DrawLine(0, 0, 0, height, BLACK); DrawLine(0, 0, 0, height, BLACK);
EndTextureMode(); EndTextureMode();
} }