From fe9a54a8da85e600df2368a0f3e6831a7e70d46b Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Fri, 6 Mar 2026 13:25:35 +0100 Subject: [PATCH] implement graph node mouse collisions (unoptimized iterating over all masses for now) --- include/input_handler.hpp | 4 + include/renderer.hpp | 27 +- src/gpu_example.cpp | 900 ++++++++++++++++++++++++++++++++++++++ src/input_handler.cpp | 13 +- src/renderer.cpp | 40 +- 5 files changed, 965 insertions(+), 19 deletions(-) create mode 100644 src/gpu_example.cpp diff --git a/include/input_handler.hpp b/include/input_handler.hpp index 6d29be2..2e38a82 100644 --- a/include/input_handler.hpp +++ b/include/input_handler.hpp @@ -87,6 +87,9 @@ public: Vector2 mouse = Vector2Zero(); Vector2 last_mouse = Vector2Zero(); + // State selection from graph + size_t collision_mass = -1; + public: input_handler(state_manager& _state, orbit_camera& _camera) : state(_state), camera(_camera) { @@ -123,6 +126,7 @@ public: auto add_block() -> void; auto remove_block() -> void; auto place_goal() const -> void; + auto select_state() const -> void; // Key actions auto toggle_camera_lock() -> void; diff --git a/include/renderer.hpp b/include/renderer.hpp index 801c988..884e616 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -14,16 +14,14 @@ class renderer { private: const state_manager& state; - const input_handler& input; + input_handler& input; user_interface& gui; const orbit_camera& camera; - RenderTexture graph_target = - LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); + RenderTexture graph_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); // TODO: Those should be moved to the user_interface.h - RenderTexture klotski_target = - LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); + RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT); // Batching @@ -35,21 +33,21 @@ private: std::vector colors; Material vertex_mat = LoadMaterialDefault(); Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); - Shader instancing_shader = - LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl"); - + Shader instancing_shader = LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl"); unsigned int color_vbo_id = 0; public: - renderer(const orbit_camera& _camera, const state_manager& _state, const input_handler& _input, + renderer(const orbit_camera& _camera, + const state_manager& _state, + input_handler& _input, user_interface& _gui) : state(_state), input(_input), gui(_gui), camera(_camera) { instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp"); - instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = - GetShaderLocationAttrib(instancing_shader, "instanceTransform"); - instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = - GetShaderLocation(instancing_shader, "viewPos"); + instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = GetShaderLocationAttrib( + instancing_shader, + "instanceTransform"); + instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(instancing_shader, "viewPos"); // infoln("LOC vertexPosition: {}", // rlGetLocationAttrib(instancing_shader.id, "vertexPosition")); @@ -102,8 +100,7 @@ private: auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void; public: - auto render(const std::vector& masses, int fps, int ups, size_t mass_count, - size_t spring_count) -> void; + auto render(const std::vector& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void; }; #endif \ No newline at end of file diff --git a/src/gpu_example.cpp b/src/gpu_example.cpp new file mode 100644 index 0000000..e304704 --- /dev/null +++ b/src/gpu_example.cpp @@ -0,0 +1,900 @@ +// gpu_force_layout_raylib_rlgl.cpp +// +// Single-file GPU force-directed layout example for raylib + rlgl + OpenGL 4.3 compute shaders. +// Includes: +// - grid-based repulsion (GPU) +// - Hooke springs from edge pairs (GPU) +// - integration (GPU) +// - point rendering directly from GPU buffer +// +// Requires: +// - raylib built with GRAPHICS_API_OPENGL_43 +// - desktop OpenGL 4.3+ +// - GL_ARB_shader_atomic_float (present on RTX 4070) +// +// Build example: +// g++ -std=c++2b -O2 gpu_force_layout_raylib_rlgl.cpp -lraylib -lGL -ldl -lpthread -lm -o +// gpu_layout +// +// Notes: +// - This is a practical starter architecture, not a finished engine. +// - For very large graphs, add multilevel coarsening and a coarse second grid. +// + +#include "raylib.h" +#include "raymath.h" +#include "rlgl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// rlgl exposes OpenGL headers on desktop builds +#ifndef GRAPHICS_API_OPENGL_43 +// You can still compile this file, but compute shaders will not work unless raylib/rlgl +// was built with OpenGL 4.3 support. +#endif + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +static void Require(bool cond, const char* msg) +{ + if (!cond) throw std::runtime_error(msg); +} + +static bool HasExtension(const char* extName) +{ + GLint count = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + for (GLint i = 0; i < count; ++i) { + const char* ext = reinterpret_cast(glGetStringi(GL_EXTENSIONS, (GLuint)i)); + if (ext && std::strcmp(ext, extName) == 0) return true; + } + return false; +} + +static unsigned int BuildComputeProgramRlgl(const char* source) +{ + unsigned int shaderId = rlCompileShader(source, RL_COMPUTE_SHADER); + Require(shaderId != 0, "Failed to compile compute shader"); + unsigned int programId = rlLoadComputeShaderProgram(shaderId); + Require(programId != 0, "Failed to link compute shader program"); + rlUnloadShader(shaderId); + return programId; +} + +static unsigned int BuildGraphicsProgramRlgl(const char* vs, const char* fs) +{ + unsigned int vsId = rlCompileShader(vs, RL_VERTEX_SHADER); + Require(vsId != 0, "Failed to compile vertex shader"); + + unsigned int fsId = rlCompileShader(fs, RL_FRAGMENT_SHADER); + Require(fsId != 0, "Failed to compile fragment shader"); + + unsigned int progId = rlLoadShaderProgram(vsId, fsId); + Require(progId != 0, "Failed to link graphics shader program"); + + rlUnloadShader(vsId); + rlUnloadShader(fsId); + return progId; +} + +static void Dispatch1D(unsigned int count, unsigned int localSizeX = 256) +{ + unsigned int groupsX = (count + localSizeX - 1) / localSizeX; + rlComputeShaderDispatch(groupsX, 1, 1); +} + +static Matrix MatrixFromCamera(const Camera3D& cam) +{ + return GetCameraMatrix(cam); +} + +//------------------------------------------------------------------------------ +// GPU data layouts (std430-friendly) +//------------------------------------------------------------------------------ + +struct ParticleGPU +{ + float pos_mass[4]; // xyz position, w mass + float vel_pad[4]; // xyz velocity + float force_pad[4]; // xyz accumulated force +}; +static_assert(sizeof(ParticleGPU) == 48); + +struct CellAccumGPU +{ + float sum_mass[4]; // xyz = sum(pos * mass), w = total mass + std::uint32_t count; + std::uint32_t pad[3]; +}; +static_assert(sizeof(CellAccumGPU) == 32); + +struct CellFinalGPU +{ + float com_mass[4]; // xyz = center of mass, w = total mass + std::uint32_t count; + std::uint32_t pad[3]; +}; +static_assert(sizeof(CellFinalGPU) == 32); + +struct EdgeGPU +{ + std::uint32_t u; + std::uint32_t v; + float weight; + float restLength; +}; +static_assert(sizeof(EdgeGPU) == 16); + +//------------------------------------------------------------------------------ +// Compute shaders +//------------------------------------------------------------------------------ + +static const char* CS_CLEAR_GRID = R"GLSL( +#version 430 +layout(local_size_x = 256) in; + +struct CellAccum { + vec4 sum_mass; + uvec4 count_pad; +}; + +layout(std430, binding=0) buffer GridAccumBuffer { + CellAccum acc[]; +}; + +uniform uint cellCount; + +void main() +{ + uint id = gl_GlobalInvocationID.x; + if (id >= cellCount) return; + + acc[id].sum_mass = vec4(0.0); + acc[id].count_pad = uvec4(0u); +} +)GLSL"; + +static const char* CS_ACCUM_GRID = R"GLSL( +#version 430 +#extension GL_ARB_shader_atomic_float : enable +layout(local_size_x = 256) in; + +struct Particle { + vec4 pos_mass; + vec4 vel_pad; + vec4 force_pad; +}; + +struct CellAccum { + vec4 sum_mass; + uvec4 count_pad; +}; + +layout(std430, binding=0) buffer ParticleBuffer { + Particle particles[]; +}; + +layout(std430, binding=1) buffer GridAccumBuffer { + CellAccum acc[]; +}; + +uniform uint particleCount; +uniform vec3 gridMin; +uniform float cellSize; +uniform ivec3 gridDim; + +int cellIndex(ivec3 c, ivec3 dim) +{ + return c.x + c.y*dim.x + c.z*dim.x*dim.y; +} + +void main() +{ + uint id = gl_GlobalInvocationID.x; + if (id >= particleCount) return; + + vec3 pos = particles[id].pos_mass.xyz; + float m = particles[id].pos_mass.w; + + ivec3 c = ivec3(floor((pos - gridMin) / cellSize)); + c = clamp(c, ivec3(0), gridDim - ivec3(1)); + + int idx = cellIndex(c, gridDim); + + atomicAdd(acc[idx].sum_mass.x, pos.x * m); + atomicAdd(acc[idx].sum_mass.y, pos.y * m); + atomicAdd(acc[idx].sum_mass.z, pos.z * m); + atomicAdd(acc[idx].sum_mass.w, m); + atomicAdd(acc[idx].count_pad.x, 1u); +} +)GLSL"; + +static const char* CS_FINALIZE_GRID = R"GLSL( +#version 430 +layout(local_size_x = 256) in; + +struct CellAccum { + vec4 sum_mass; + uvec4 count_pad; +}; + +struct CellFinal { + vec4 com_mass; + uvec4 count_pad; +}; + +layout(std430, binding=0) buffer GridAccumBuffer { + CellAccum acc[]; +}; + +layout(std430, binding=1) buffer GridFinalBuffer { + CellFinal cell[]; +}; + +uniform uint cellCount; + +void main() +{ + uint id = gl_GlobalInvocationID.x; + if (id >= cellCount) return; + + float mass = acc[id].sum_mass.w; + uint count = acc[id].count_pad.x; + + vec3 com = vec3(0.0); + if (mass > 0.0) { + com = acc[id].sum_mass.xyz / mass; + } + + cell[id].com_mass = vec4(com, mass); + cell[id].count_pad = uvec4(count, 0u, 0u, 0u); +} +)GLSL"; + +static const char* CS_REPULSION = R"GLSL( +#version 430 +layout(local_size_x = 256) in; + +struct Particle { + vec4 pos_mass; + vec4 vel_pad; + vec4 force_pad; +}; + +struct CellFinal { + vec4 com_mass; + uvec4 count_pad; +}; + +layout(std430, binding=0) buffer ParticleBuffer { + Particle particles[]; +}; + +layout(std430, binding=1) buffer GridFinalBuffer { + CellFinal cell[]; +}; + +uniform uint particleCount; +uniform vec3 gridMin; +uniform float cellSize; +uniform ivec3 gridDim; + +uniform int radiusCells; +uniform float softening; +uniform float repulsionK2; +uniform float maxRepulsionForce; +uniform float selfCellFactor; + +int cellIndex(ivec3 c, ivec3 dim) +{ + return c.x + c.y*dim.x + c.z*dim.x*dim.y; +} + +void main() +{ + uint id = gl_GlobalInvocationID.x; + if (id >= particleCount) return; + + vec3 pos = particles[id].pos_mass.xyz; + ivec3 base = ivec3(floor((pos - gridMin) / cellSize)); + base = clamp(base, ivec3(0), gridDim - ivec3(1)); + + vec3 force = vec3(0.0); + + for (int dz = -radiusCells; dz <= radiusCells; ++dz) + for (int dy = -radiusCells; dy <= radiusCells; ++dy) + for (int dx = -radiusCells; dx <= radiusCells; ++dx) + { + ivec3 c = base + ivec3(dx, dy, dz); + if (any(lessThan(c, ivec3(0))) || any(greaterThanEqual(c, gridDim))) continue; + + int idx = cellIndex(c, gridDim); + float mass = cell[idx].com_mass.w; + if (mass <= 0.0) continue; + + vec3 com = cell[idx].com_mass.xyz; + vec3 d = pos - com; + + float r2 = dot(d, d) + softening; + float factor = ((dx == 0) && (dy == 0) && (dz == 0)) ? selfCellFactor : 1.0; + + // k^2 / r magnitude with direction d/r = k^2 * d / r^2 + float invR = inversesqrt(r2); + force += factor * (repulsionK2 * mass) * d * (invR * invR); + } + + float len = length(force); + if (len > maxRepulsionForce) { + force *= (maxRepulsionForce / len); + } + + particles[id].force_pad.xyz = force; +} +)GLSL"; + +static const char* CS_SPRINGS = R"GLSL( +#version 430 +#extension GL_ARB_shader_atomic_float : enable +layout(local_size_x = 256) in; + +struct Particle { + vec4 pos_mass; + vec4 vel_pad; + vec4 force_pad; +}; + +struct Edge { + uint u; + uint v; + float weight; + float restLength; +}; + +layout(std430, binding=0) buffer ParticleBuffer { + Particle particles[]; +}; + +layout(std430, binding=1) buffer EdgeBuffer { + Edge edges[]; +}; + +uniform uint edgeCount; +uniform float springK; +uniform float maxSpringForce; + +void main() +{ + uint id = gl_GlobalInvocationID.x; + if (id >= edgeCount) return; + + Edge e = edges[id]; + vec3 pu = particles[e.u].pos_mass.xyz; + vec3 pv = particles[e.v].pos_mass.xyz; + + vec3 d = pv - pu; + float dist = length(d) + 1e-6; + vec3 dir = d / dist; + + // Hooke: F = k * (dist - rest) + float stretch = dist - e.restLength; + float mag = springK * e.weight * stretch; + mag = clamp(mag, -maxSpringForce, maxSpringForce); + + vec3 F = mag * dir; + + atomicAdd(particles[e.u].force_pad.x, F.x); + atomicAdd(particles[e.u].force_pad.y, F.y); + atomicAdd(particles[e.u].force_pad.z, F.z); + + atomicAdd(particles[e.v].force_pad.x, -F.x); + atomicAdd(particles[e.v].force_pad.y, -F.y); + atomicAdd(particles[e.v].force_pad.z, -F.z); +} +)GLSL"; + +static const char* CS_INTEGRATE = R"GLSL( +#version 430 +layout(local_size_x = 256) in; + +struct Particle { + vec4 pos_mass; + vec4 vel_pad; + vec4 force_pad; +}; + +layout(std430, binding=0) buffer ParticleBuffer { + Particle particles[]; +}; + +uniform uint particleCount; +uniform float dt; +uniform float damping; +uniform float stepScale; + +void main() +{ + uint id = gl_GlobalInvocationID.x; + if (id >= particleCount) return; + + vec3 vel = particles[id].vel_pad.xyz; + vec3 force = particles[id].force_pad.xyz; + + vel = (vel + dt * force) * damping; + vec3 pos = particles[id].pos_mass.xyz + (dt * stepScale) * vel; + + particles[id].vel_pad.xyz = vel; + particles[id].pos_mass.xyz = pos; + particles[id].force_pad.xyz = vec3(0.0); +} +)GLSL"; + +//------------------------------------------------------------------------------ +// Rendering shaders +//------------------------------------------------------------------------------ + +static const char* VS_POINTS = R"GLSL( +#version 430 +layout(location=0) in vec3 aPos; + +uniform mat4 uMVP; +uniform float uPointSize; + +void main() +{ + gl_Position = uMVP * vec4(aPos, 1.0); + gl_PointSize = uPointSize; +} +)GLSL"; + +static const char* FS_POINTS = R"GLSL( +#version 430 +out vec4 FragColor; + +void main() +{ + vec2 p = gl_PointCoord * 2.0 - 1.0; + if (dot(p, p) > 1.0) discard; + FragColor = vec4(0.90, 0.95, 1.00, 1.00); +} +)GLSL"; + +//------------------------------------------------------------------------------ +// Main +//------------------------------------------------------------------------------ + +int main() +{ + SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE); + InitWindow(1400, 900, "raylib rlgl GPU force-directed layout"); + + TraceLog(LOG_INFO, "GL_VERSION: %s", glGetString(GL_VERSION)); + TraceLog(LOG_INFO, "GLSL_VERSION: %s", glGetString(GL_SHADING_LANGUAGE_VERSION)); + + try { + Require(HasExtension("GL_ARB_compute_shader"), + "Missing GL_ARB_compute_shader. Build raylib with OpenGL 4.3."); + Require(HasExtension("GL_ARB_shader_storage_buffer_object"), + "Missing GL_ARB_shader_storage_buffer_object."); + Require(HasExtension("GL_ARB_shader_atomic_float"), "Missing GL_ARB_shader_atomic_float."); + + //-------------------------------------------------------------------------- + // Simulation parameters + //-------------------------------------------------------------------------- + + const std::uint32_t nodeCount = 30000; // change to 100000 on your machine + const std::uint32_t approxEdgeCount = nodeCount * 2; + + const int gridX = 64; + const int gridY = 64; + const int gridZ = 64; + const std::uint32_t gridCellCount = std::uint32_t(gridX * gridY * gridZ); + + Vector3 simMin{-40.0f, -40.0f, -40.0f}; + Vector3 simMax{40.0f, 40.0f, 40.0f}; + Vector3 simExtent = Vector3Subtract(simMax, simMin); + + float cellSize = std::max( + {simExtent.x / float(gridX), simExtent.y / float(gridY), simExtent.z / float(gridZ)}); + + float dt = 0.020f; + float damping = 0.90f; + float stepScale = 1.0f; + float repulsionK2 = 0.35f; + float springK = 0.18f; + float springRest = 1.2f; + float softening = 0.02f; + float maxRepulsionForce = 60.0f; + float maxSpringForce = 40.0f; + float selfCellFactor = 0.15f; + int radiusCells = 2; + + //-------------------------------------------------------------------------- + // CPU-side initial data + //-------------------------------------------------------------------------- + + std::vector particles(nodeCount); + for (std::uint32_t i = 0; i < nodeCount; ++i) { + float rx = float(GetRandomValue(-10000, 10000)) / 10000.0f; + float ry = float(GetRandomValue(-10000, 10000)) / 10000.0f; + float rz = float(GetRandomValue(-10000, 10000)) / 10000.0f; + + Vector3 p{simMin.x + (rx * 0.5f + 0.5f) * simExtent.x, + simMin.y + (ry * 0.5f + 0.5f) * simExtent.y, + simMin.z + (rz * 0.5f + 0.5f) * simExtent.z}; + + particles[i].pos_mass[0] = p.x; + particles[i].pos_mass[1] = p.y; + particles[i].pos_mass[2] = p.z; + particles[i].pos_mass[3] = 1.0f; + + particles[i].vel_pad[0] = 0.0f; + particles[i].vel_pad[1] = 0.0f; + particles[i].vel_pad[2] = 0.0f; + particles[i].vel_pad[3] = 0.0f; + + particles[i].force_pad[0] = 0.0f; + particles[i].force_pad[1] = 0.0f; + particles[i].force_pad[2] = 0.0f; + particles[i].force_pad[3] = 0.0f; + } + + std::vector edges; + edges.reserve(approxEdgeCount); + + // Example spring graph: + // - chain edge + // - one random edge + // This gives a graph-like structure instead of pure gas. + for (std::uint32_t i = 0; i + 1 < nodeCount; ++i) { + edges.push_back(EdgeGPU{i, i + 1, 1.0f, springRest}); + } + + for (std::uint32_t i = 0; i < nodeCount; ++i) { + std::uint32_t j = std::uint32_t(GetRandomValue(0, int(nodeCount - 1))); + if (j != i) { + edges.push_back(EdgeGPU{i, j, 1.0f, springRest}); + } + } + + const std::uint32_t edgeCount = (std::uint32_t)edges.size(); + + //-------------------------------------------------------------------------- + // GPU buffers via rlgl + //-------------------------------------------------------------------------- + + unsigned int particleSsbo = + rlLoadShaderBuffer((unsigned int)(particles.size() * sizeof(ParticleGPU)), + particles.data(), RL_DYNAMIC_COPY); + + unsigned int gridAccumSsbo = rlLoadShaderBuffer( + gridCellCount * (unsigned int)sizeof(CellAccumGPU), nullptr, RL_DYNAMIC_COPY); + + unsigned int gridFinalSsbo = rlLoadShaderBuffer( + gridCellCount * (unsigned int)sizeof(CellFinalGPU), nullptr, RL_DYNAMIC_COPY); + + unsigned int edgeSsbo = rlLoadShaderBuffer((unsigned int)(edges.size() * sizeof(EdgeGPU)), + edges.data(), RL_STATIC_DRAW); + + Require(particleSsbo != 0, "Failed to create particle SSBO"); + Require(gridAccumSsbo != 0, "Failed to create gridAccum SSBO"); + Require(gridFinalSsbo != 0, "Failed to create gridFinal SSBO"); + Require(edgeSsbo != 0, "Failed to create edge SSBO"); + + //-------------------------------------------------------------------------- + // Programs via rlgl + //-------------------------------------------------------------------------- + + unsigned int progClear = BuildComputeProgramRlgl(CS_CLEAR_GRID); + unsigned int progAccum = BuildComputeProgramRlgl(CS_ACCUM_GRID); + unsigned int progFinalize = BuildComputeProgramRlgl(CS_FINALIZE_GRID); + unsigned int progRepel = BuildComputeProgramRlgl(CS_REPULSION); + unsigned int progSprings = BuildComputeProgramRlgl(CS_SPRINGS); + unsigned int progIntegrate = BuildComputeProgramRlgl(CS_INTEGRATE); + unsigned int progPoints = BuildGraphicsProgramRlgl(VS_POINTS, FS_POINTS); + + //-------------------------------------------------------------------------- + // Rendering state + //-------------------------------------------------------------------------- + + GLuint vao = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + glBindBuffer(GL_ARRAY_BUFFER, particleSsbo); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, (GLsizei)sizeof(ParticleGPU), + (const void*)0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + glBindVertexArray(0); + + Camera3D camera{}; + camera.position = {0.0f, 0.0f, 110.0f}; + camera.target = {0.0f, 0.0f, 0.0f}; + camera.up = {0.0f, 1.0f, 0.0f}; + camera.fovy = 60.0f; + camera.projection = CAMERA_PERSPECTIVE; + + SetTargetFPS(60); + + float orbit = 0.0f; + + //-------------------------------------------------------------------------- + // Main loop + //-------------------------------------------------------------------------- + + while (!WindowShouldClose()) { + // Simple interactive tuning + if (IsKeyPressed(KEY_ONE)) radiusCells = 1; + if (IsKeyPressed(KEY_TWO)) radiusCells = 2; + if (IsKeyPressed(KEY_THREE)) radiusCells = 3; + + if (IsKeyDown(KEY_Q)) repulsionK2 *= 1.01f; + if (IsKeyDown(KEY_A)) repulsionK2 *= 0.99f; + + if (IsKeyDown(KEY_W)) springK *= 1.01f; + if (IsKeyDown(KEY_S)) springK *= 0.99f; + + if (IsKeyDown(KEY_E)) damping = std::min(0.999f, damping + 0.0005f); + if (IsKeyDown(KEY_D)) damping = std::max(0.70f, damping - 0.0005f); + + if (IsKeyPressed(KEY_SPACE)) stepScale = 1.0f; + stepScale = std::max(0.10f, stepScale * 0.9994f); + + orbit += 0.0020f; + camera.position = {110.0f * std::sin(orbit), 35.0f, 110.0f * std::cos(orbit)}; + + // Flush raylib draw batch before raw GL / compute work + rlDrawRenderBatchActive(); + + // ------------------------------------------------------------ + // Pass 1: clear grid accum + // ------------------------------------------------------------ + rlEnableShader(progClear); + rlBindShaderBuffer(gridAccumSsbo, 0); + + { + unsigned int u = (unsigned int)gridCellCount; + int loc = rlGetLocationUniform(progClear, "cellCount"); + rlSetUniform(loc, &u, RL_SHADER_UNIFORM_UINT, 1); + } + + Dispatch1D(gridCellCount); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + // ------------------------------------------------------------ + // Pass 2: accumulate particles into grid + // ------------------------------------------------------------ + rlEnableShader(progAccum); + rlBindShaderBuffer(particleSsbo, 0); + rlBindShaderBuffer(gridAccumSsbo, 1); + + { + unsigned int u = nodeCount; + int loc = rlGetLocationUniform(progAccum, "particleCount"); + rlSetUniform(loc, &u, RL_SHADER_UNIFORM_UINT, 1); + } + { + int loc = rlGetLocationUniform(progAccum, "gridMin"); + float v[3] = {simMin.x, simMin.y, simMin.z}; + rlSetUniform(loc, v, RL_SHADER_UNIFORM_VEC3, 1); + } + { + int loc = rlGetLocationUniform(progAccum, "cellSize"); + rlSetUniform(loc, &cellSize, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progAccum, "gridDim"); + int v[3] = {gridX, gridY, gridZ}; + rlSetUniform(loc, v, RL_SHADER_UNIFORM_IVEC3, 1); + } + + Dispatch1D(nodeCount); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + // ------------------------------------------------------------ + // Pass 3: finalize cell COM + // ------------------------------------------------------------ + rlEnableShader(progFinalize); + rlBindShaderBuffer(gridAccumSsbo, 0); + rlBindShaderBuffer(gridFinalSsbo, 1); + + { + unsigned int u = (unsigned int)gridCellCount; + int loc = rlGetLocationUniform(progFinalize, "cellCount"); + rlSetUniform(loc, &u, RL_SHADER_UNIFORM_UINT, 1); + } + + Dispatch1D(gridCellCount); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + // ------------------------------------------------------------ + // Pass 4: repulsion + // ------------------------------------------------------------ + rlEnableShader(progRepel); + rlBindShaderBuffer(particleSsbo, 0); + rlBindShaderBuffer(gridFinalSsbo, 1); + + { + unsigned int u = nodeCount; + int loc = rlGetLocationUniform(progRepel, "particleCount"); + rlSetUniform(loc, &u, RL_SHADER_UNIFORM_UINT, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "gridMin"); + float v[3] = {simMin.x, simMin.y, simMin.z}; + rlSetUniform(loc, v, RL_SHADER_UNIFORM_VEC3, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "cellSize"); + rlSetUniform(loc, &cellSize, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "gridDim"); + int v[3] = {gridX, gridY, gridZ}; + rlSetUniform(loc, v, RL_SHADER_UNIFORM_IVEC3, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "radiusCells"); + rlSetUniform(loc, &radiusCells, RL_SHADER_UNIFORM_INT, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "softening"); + rlSetUniform(loc, &softening, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "repulsionK2"); + rlSetUniform(loc, &repulsionK2, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "maxRepulsionForce"); + rlSetUniform(loc, &maxRepulsionForce, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progRepel, "selfCellFactor"); + rlSetUniform(loc, &selfCellFactor, RL_SHADER_UNIFORM_FLOAT, 1); + } + + Dispatch1D(nodeCount); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + // ------------------------------------------------------------ + // Pass 5: springs + // ------------------------------------------------------------ + rlEnableShader(progSprings); + rlBindShaderBuffer(particleSsbo, 0); + rlBindShaderBuffer(edgeSsbo, 1); + + { + unsigned int u = edgeCount; + int loc = rlGetLocationUniform(progSprings, "edgeCount"); + rlSetUniform(loc, &u, RL_SHADER_UNIFORM_UINT, 1); + } + { + int loc = rlGetLocationUniform(progSprings, "springK"); + rlSetUniform(loc, &springK, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progSprings, "maxSpringForce"); + rlSetUniform(loc, &maxSpringForce, RL_SHADER_UNIFORM_FLOAT, 1); + } + + Dispatch1D(edgeCount); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + // ------------------------------------------------------------ + // Pass 6: integrate + // ------------------------------------------------------------ + rlEnableShader(progIntegrate); + rlBindShaderBuffer(particleSsbo, 0); + + { + unsigned int u = nodeCount; + int loc = rlGetLocationUniform(progIntegrate, "particleCount"); + rlSetUniform(loc, &u, RL_SHADER_UNIFORM_UINT, 1); + } + { + int loc = rlGetLocationUniform(progIntegrate, "dt"); + rlSetUniform(loc, &dt, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progIntegrate, "damping"); + rlSetUniform(loc, &damping, RL_SHADER_UNIFORM_FLOAT, 1); + } + { + int loc = rlGetLocationUniform(progIntegrate, "stepScale"); + rlSetUniform(loc, &stepScale, RL_SHADER_UNIFORM_FLOAT, 1); + } + + Dispatch1D(nodeCount); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT); + + rlDisableShader(); + + // ------------------------------------------------------------ + // Draw + // ------------------------------------------------------------ + BeginDrawing(); + ClearBackground(Color{9, 11, 16, 255}); + + BeginMode3D(camera); + DrawCubeWiresV(Vector3Scale(Vector3Add(simMin, simMax), 0.5f), simExtent, + Fade(GRAY, 0.20f)); + + rlDrawRenderBatchActive(); + + Matrix view = MatrixFromCamera(camera); + Matrix proj = MatrixPerspective(DEG2RAD * camera.fovy, + (float)GetScreenWidth() / (float)GetScreenHeight(), + 0.1f, 1000.0f); + Matrix mvp = MatrixMultiply(proj, view); + + glUseProgram(progPoints); + glBindVertexArray(vao); + + GLint locMvp = glGetUniformLocation(progPoints, "uMVP"); + GLint locPt = glGetUniformLocation(progPoints, "uPointSize"); + glUniformMatrix4fv(locMvp, 1, GL_FALSE, (const float*)&mvp); + glUniform1f(locPt, 2.0f); + + glEnable(GL_PROGRAM_POINT_SIZE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_DEPTH_TEST); + + glDrawArrays(GL_POINTS, 0, (GLsizei)nodeCount); + + glBindVertexArray(0); + glUseProgram(0); + + EndMode3D(); + + DrawText(TextFormat("nodes=%u edges=%u grid=%dx%dx%d radius=%d", nodeCount, edgeCount, + gridX, gridY, gridZ, radiusCells), + 10, 10, 20, RAYWHITE); + DrawText(TextFormat("repulsionK2=%.3f springK=%.3f damping=%.3f stepScale=%.3f", + repulsionK2, springK, damping, stepScale), + 10, 36, 20, RAYWHITE); + DrawText( + "Keys: 1/2/3 radius, Q/A repulsion, W/S spring, E/D damping, SPACE reset cooling", + 10, 62, 18, Fade(RAYWHITE, 0.8f)); + + EndDrawing(); + } + + //-------------------------------------------------------------------------- + // Cleanup + //-------------------------------------------------------------------------- + + glDeleteVertexArrays(1, &vao); + + rlUnloadShaderProgram(progPoints); + rlUnloadShaderProgram(progClear); + rlUnloadShaderProgram(progAccum); + rlUnloadShaderProgram(progFinalize); + rlUnloadShaderProgram(progRepel); + rlUnloadShaderProgram(progSprings); + rlUnloadShaderProgram(progIntegrate); + + rlUnloadShaderBuffer(edgeSsbo); + rlUnloadShaderBuffer(gridFinalSsbo); + rlUnloadShaderBuffer(gridAccumSsbo); + rlUnloadShaderBuffer(particleSsbo); + } + catch (const std::exception& e) { + TraceLog(LOG_ERROR, "%s", e.what()); + } + + CloseWindow(); + return 0; +} diff --git a/src/input_handler.cpp b/src/input_handler.cpp index 976e2b0..bcd1d5a 100644 --- a/src/input_handler.cpp +++ b/src/input_handler.cpp @@ -18,6 +18,7 @@ auto input_handler::init_handlers() -> void register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::add_block); register_mouse_pressed_handler(MOUSE_BUTTON_LEFT, &input_handler::start_add_block); register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::place_goal); + register_mouse_pressed_handler(MOUSE_BUTTON_MIDDLE, &input_handler::select_state); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::camera_start_rotate); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::remove_block); register_mouse_pressed_handler(MOUSE_BUTTON_RIGHT, &input_handler::clear_add_block); @@ -245,7 +246,7 @@ auto input_handler::remove_block() -> void auto input_handler::place_goal() const -> void { const puzzle& current = state.get_current_state(); - if (!editing || !current.covers(hov_x, hov_y)) { + if (!editing || !mouse_in_board_pane() || !current.covers(hov_x, hov_y)) { return; } @@ -257,6 +258,16 @@ auto input_handler::place_goal() const -> void state.edit_starting_state(*next); } +auto input_handler::select_state() const -> void +{ + if (!mouse_in_graph_pane() || collision_mass == static_cast(-1)) { + return; + } + + const puzzle& selected = state.get_state(collision_mass); + state.update_current_state(selected); +} + auto input_handler::toggle_camera_lock() -> void { if (!camera_lock) { diff --git a/src/renderer.cpp b/src/renderer.cpp index 41af966..2f466c6 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -63,6 +63,39 @@ auto renderer::draw_mass_springs(const std::vector& masses) -> void transforms.clear(); colors.clear(); + // Collisions + // TODO: This would benefit greatly from a spatial data structure. + // Would it be worth to copy the octree from the physics thread? + input.collision_mass = -1; + if (input.mouse_in_graph_pane()) { + const Ray ray = GetScreenToWorldRayEx( + GetMousePosition() - Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), + camera.camera, graph_target.texture.width, graph_target.texture.height); + RayCollision collision; // Ray collision hit info + + size_t mass = 0; + for (const auto& [x, y, z] : masses) { + collision = GetRayCollisionBox(ray, + BoundingBox{ + { + x - VERTEX_SIZE / 2.0f, + y - VERTEX_SIZE / 2.0f, + z - VERTEX_SIZE / 2.0f + }, + { + x + VERTEX_SIZE / 2.0f, + y + VERTEX_SIZE / 2.0f, + z + VERTEX_SIZE / 2.0f + } + }); + if (collision.hit) { + input.collision_mass = mass; + break; + } + ++mass; + } + } + // Find max distance to interpolate colors in the given [0, max] range int max_distance = 0; for (const int distance : state.get_distances()) { @@ -105,9 +138,10 @@ auto renderer::draw_mass_springs(const std::vector& masses) -> void // Visited vertex c = VERTEX_VISITED_COLOR; } else if (input.color_by_distance && distances.size() == masses.size()) { - c = lerp_color(VERTEX_FARTHEST_COLOR, - VERTEX_CLOSEST_COLOR, - static_cast(distances[mass])); + c = lerp_color(VERTEX_FARTHEST_COLOR, VERTEX_CLOSEST_COLOR, static_cast(distances[mass])); + } + if (mass == input.collision_mass) { + c = RED; } // Current vertex is drawn as individual cube to increase its size