From bfe8c6d5724655a7ba4f253e16b17e58814788b9 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 24 Feb 2026 00:01:04 +0100 Subject: [PATCH] store masses/springs inside vector and manage unordered_maps for a state<->index mapping this reduces the time required to iterate over all masses/springs because data is stored in contiguous memory --- include/physics.hpp | 31 +++++-------- src/physics.cpp | 111 +++++++++++++++++--------------------------- src/renderer.cpp | 60 ++++++++++++++---------- src/state.cpp | 2 +- 4 files changed, 89 insertions(+), 115 deletions(-) diff --git a/include/physics.hpp b/include/physics.hpp index 25bfaaa..c5f4149 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -19,17 +19,15 @@ class Mass { public: - const float mass; Vector3 position; Vector3 previous_position; // for verlet integration Vector3 velocity; Vector3 force; - const bool fixed; public: - Mass(float _mass, Vector3 _position, bool _fixed) - : mass(_mass), position(_position), previous_position(_position), - velocity(Vector3Zero()), force(Vector3Zero()), fixed(_fixed) {} + Mass(Vector3 _position) + : position(_position), previous_position(_position), + velocity(Vector3Zero()), force(Vector3Zero()) {} public: auto ClearForce() -> void; @@ -43,26 +41,18 @@ public: class Spring { public: - Mass &massA; - Mass &massB; - const float spring_constant; - const float dampening_constant; - const float rest_length; + int mass_a; + int mass_b; public: - Spring(Mass &_massA, Mass &_massB, float _spring_constant, - float _dampening_constant, float _rest_length) - : massA(_massA), massB(_massB), spring_constant(_spring_constant), - dampening_constant(_dampening_constant), rest_length(_rest_length) {} + Spring(int _mass_a, int _mass_b) : mass_a(_mass_a), mass_b(_mass_b) {} public: - auto CalculateSpringForce() const -> void; + auto CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void; }; class MassSpringSystem { private: - std::vector mass_pointers; - #ifdef BARNES_HUT // Barnes-Hut Octree octree; @@ -81,9 +71,10 @@ private: public: // This is the main ownership of all the states/masses/springs. - // TODO: Everything is stored multiple times but idc (currently). - std::unordered_map masses; - std::unordered_map, Spring> springs; + std::vector masses; + std::unordered_map state_masses; + std::vector springs; + std::unordered_map, int> state_springs; public: MassSpringSystem() { diff --git a/src/physics.cpp b/src/physics.cpp index 7055cff..90f3a7b 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -19,23 +19,15 @@ auto Mass::ClearForce() -> void { force = Vector3Zero(); } auto Mass::CalculateVelocity(const float delta_time) -> void { - if (fixed) { - return; - } - Vector3 acceleration; Vector3 temp; - acceleration = Vector3Scale(force, 1.0 / mass); + acceleration = Vector3Scale(force, 1.0 / MASS); temp = Vector3Scale(acceleration, delta_time); velocity = Vector3Add(velocity, temp); } auto Mass::CalculatePosition(const float delta_time) -> void { - if (fixed) { - return; - } - previous_position = position; Vector3 temp; @@ -45,11 +37,7 @@ auto Mass::CalculatePosition(const float delta_time) -> void { } auto Mass::VerletUpdate(const float delta_time) -> void { - if (fixed) { - return; - } - - Vector3 acceleration = Vector3Scale(force, 1.0 / mass); + Vector3 acceleration = Vector3Scale(force, 1.0 / MASS); Vector3 temp_position = position; Vector3 displacement = Vector3Subtract(position, previous_position); @@ -62,14 +50,14 @@ auto Mass::VerletUpdate(const float delta_time) -> void { previous_position = temp_position; } -auto Spring::CalculateSpringForce() const -> void { - Vector3 delta_position = Vector3Subtract(massA.position, massB.position); +auto Spring::CalculateSpringForce(Mass &_mass_a, Mass &_mass_b) const -> void { + Vector3 delta_position = Vector3Subtract(_mass_a.position, _mass_b.position); float current_length = Vector3Length(delta_position); float inv_current_length = 1.0 / current_length; - Vector3 delta_velocity = Vector3Subtract(massA.velocity, massB.velocity); + Vector3 delta_velocity = Vector3Subtract(_mass_a.velocity, _mass_b.velocity); - float hooke = spring_constant * (current_length - rest_length); - float dampening = dampening_constant * + float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH); + float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) * inv_current_length; @@ -77,53 +65,59 @@ auto Spring::CalculateSpringForce() const -> void { Vector3Scale(delta_position, -(hooke + dampening) * inv_current_length); Vector3 force_b = Vector3Scale(force_a, -1.0); - massA.force = Vector3Add(massA.force, force_a); - massB.force = Vector3Add(massB.force, force_b); + _mass_a.force = Vector3Add(_mass_a.force, force_a); + _mass_b.force = Vector3Add(_mass_b.force, force_b); } auto MassSpringSystem::AddMass(float mass, bool fixed, const State &state) -> void { - if (!masses.contains(state)) { - masses.insert( - std::make_pair(state.state, Mass(mass, Vector3Zero(), fixed))); + if (!state_masses.contains(state)) { + masses.emplace_back(Vector3Zero()); + std::size_t idx = masses.size() - 1; + state_masses.insert(std::make_pair(state, idx)); } } auto MassSpringSystem::GetMass(const State &state) -> Mass & { - return masses.at(state); + return masses.at(state_masses.at(state)); } auto MassSpringSystem::GetMass(const State &state) const -> const Mass & { - return masses.at(state); + return masses.at(state_masses.at(state)); } -auto MassSpringSystem::AddSpring(const State &massA, const State &massB, +auto MassSpringSystem::AddSpring(const State &state_a, const State &state_b, float spring_constant, float dampening_constant, float rest_length) -> void { - std::pair key = std::make_pair(massA, massB); - if (!springs.contains(key)) { - Mass &a = GetMass(massA); - Mass &b = GetMass(massB); + std::pair key = std::make_pair(state_a, state_b); + if (!state_springs.contains(key)) { + int a = state_masses.at(state_a); + int b = state_masses.at(state_b); + const Mass &mass_a = masses.at(a); + Mass &mass_b = masses.at(b); - Vector3 position = a.position; + Vector3 position = mass_a.position; Vector3 offset = Vector3(static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100))); offset = Vector3Scale(Vector3Normalize(offset), REST_LENGTH); - if (b.position == Vector3Zero()) { - b.position = Vector3Add(position, offset); + if (mass_b.position == Vector3Zero()) { + mass_b.position = Vector3Add(position, offset); } - springs.insert(std::make_pair( - key, Spring(a, b, spring_constant, dampening_constant, rest_length))); + springs.emplace_back(a, b); + int idx = springs.size() - 1; + state_springs.insert(std::make_pair(key, idx)); } } auto MassSpringSystem::Clear() -> void { masses.clear(); + state_masses.clear(); springs.clear(); + state_springs.clear(); #ifndef BARNES_HUT InvalidateGrid(); #endif @@ -132,7 +126,7 @@ auto MassSpringSystem::Clear() -> void { auto MassSpringSystem::ClearForces() -> void { ZoneScoped; - for (auto &[state, mass] : masses) { + for (auto &mass : masses) { mass.ClearForce(); } } @@ -140,23 +134,11 @@ auto MassSpringSystem::ClearForces() -> void { auto MassSpringSystem::CalculateSpringForces() -> void { ZoneScoped; - for (auto &[states, spring] : springs) { - spring.CalculateSpringForce(); + for (const auto spring : springs) { + Mass &a = masses.at(spring.mass_a); + Mass &b = masses.at(spring.mass_b); + spring.CalculateSpringForce(a, b); } - - // spring_pointers.clear(); - // spring_pointers.reserve(springs.size()); - // for (auto &[states, spring] : springs) { - // spring_pointers.push_back(&spring); - // } - // - // auto solve_spring = [&](int i) { - // spring_pointers[i]->CalculateSpringForce(); - // }; - // - // BS::multi_future loop_future = - // threads.submit_loop(0, spring_pointers.size(), solve_spring, 4096); - // loop_future.wait(); } #ifdef BARNES_HUT @@ -169,7 +151,7 @@ auto MassSpringSystem::BuildOctree() -> void { // Compute bounding box around all masses Vector3 min = Vector3(FLT_MAX, FLT_MAX, FLT_MAX); Vector3 max = Vector3(-FLT_MAX, -FLT_MAX, -FLT_MAX); - for (const auto &[state, mass] : masses) { + for (const auto &mass : masses) { min.x = std::min(min.x, mass.position.x); max.x = std::max(max.x, mass.position.x); min.y = std::min(min.y, mass.position.y); @@ -190,15 +172,8 @@ auto MassSpringSystem::BuildOctree() -> void { // Root node spans the entire area int root = octree.CreateNode(min, max); - // Use a vector of pointers to the masses, because we can't parallelize the - // range-based for loop over the masses unordered_map using OpenMP. - mass_pointers.clear(); - mass_pointers.reserve(masses.size()); - for (auto &[state, mass] : masses) { - mass_pointers.push_back(&mass); - } - for (std::size_t i = 0; i < mass_pointers.size(); ++i) { - octree.Insert(root, i, mass_pointers[i]->position, mass_pointers[i]->mass); + for (std::size_t i = 0; i < masses.size(); ++i) { + octree.Insert(root, i, masses[i].position, MASS); } } @@ -251,10 +226,8 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void { BuildOctree(); auto solve_octree = [&](int i) { - int root = 0; - Vector3 force = octree.CalculateForce(root, mass_pointers[i]->position); - - mass_pointers[i]->force = Vector3Add(mass_pointers[i]->force, force); + Vector3 force = octree.CalculateForce(0, masses[i].position); + masses[i].force = Vector3Add(masses[i].force, force); }; // Calculate forces using Barnes-Hut @@ -264,7 +237,7 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void { } #else BS::multi_future loop_future = - threads.submit_loop(0, mass_pointers.size(), solve_octree, 256); + threads.submit_loop(0, masses.size(), solve_octree, 256); loop_future.wait(); #endif @@ -352,7 +325,7 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void { auto MassSpringSystem::VerletUpdate(float delta_time) -> void { ZoneScoped; - for (auto &[state, mass] : masses) { + for (auto &mass : masses) { mass.VerletUpdate(delta_time); } } diff --git a/src/renderer.cpp b/src/renderer.cpp index 7dae4b8..b21155f 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -70,17 +70,20 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, ZoneScoped; // Prepare cube instancing - if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { - if (transforms == nullptr) { - AllocateGraphInstancing(mass_springs); - } - ReallocateGraphInstancingIfNecessary(mass_springs); + { + ZoneNamedN(prepare_masses, "PrepareMasses", true); + if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { + if (transforms == nullptr) { + AllocateGraphInstancing(mass_springs); + } + ReallocateGraphInstancingIfNecessary(mass_springs); - int i = 0; - for (const auto &[state, mass] : mass_springs.masses) { - transforms[i] = - MatrixTranslate(mass.position.x, mass.position.y, mass.position.z); - ++i; + int i = 0; + for (const auto &mass : mass_springs.masses) { + transforms[i] = + MatrixTranslate(mass.position.x, mass.position.y, mass.position.z); + ++i; + } } } @@ -90,24 +93,31 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &mass_springs, BeginMode3D(camera.camera); // Draw springs (batched) - rlBegin(RL_LINES); - for (const auto &[states, spring] : mass_springs.springs) { - rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); - rlVertex3f(spring.massA.position.x, spring.massA.position.y, - spring.massA.position.z); - rlVertex3f(spring.massB.position.x, spring.massB.position.y, - spring.massB.position.z); + { + ZoneNamedN(draw_springs, "DrawSprings", true); + rlBegin(RL_LINES); + for (const auto &spring : mass_springs.springs) { + // We have to do a lookup of the actual mass object, which is slow :( + const Mass &a = mass_springs.masses.at(spring.mass_a); + const Mass &b = mass_springs.masses.at(spring.mass_b); + rlColor4ub(EDGE_COLOR.r, EDGE_COLOR.g, EDGE_COLOR.b, EDGE_COLOR.a); + rlVertex3f(a.position.x, a.position.y, a.position.z); + rlVertex3f(b.position.x, b.position.y, b.position.z); + } + rlEnd(); } - rlEnd(); // Draw masses (instanced) - if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { - // NOTE: I don't know if drawing all this inside a shader would make it much - // faster... - // The amount of data sent to the GPU would be reduced (just positions - // instead of matrices), but is this noticable for < 100000 cubes? - DrawMeshInstanced(cube_instance, vertex_mat, transforms, - mass_springs.masses.size()); + { + ZoneNamedN(draw_masses, "DrawMasses", true); + if (mass_springs.masses.size() < DRAW_VERTICES_LIMIT) { + // NOTE: I don't know if drawing all this inside a shader would make it + // much faster... The amount of data sent to the GPU would be + // reduced (just positions instead of matrices), but is this + // noticable for < 100000 cubes? + DrawMeshInstanced(cube_instance, vertex_mat, transforms, + mass_springs.masses.size()); + } } // Mark winning states diff --git a/src/state.cpp b/src/state.cpp index 41faaca..4ca7007 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -84,7 +84,7 @@ auto StateManager::ClearGraph() -> void { auto StateManager::FindWinningStates() -> void { winning_states.clear(); - for (const auto &[state, mass] : mass_springs.masses) { + for (const auto &[state, mass] : mass_springs.state_masses) { if (win_conditions[current_preset](state)) { winning_states.insert(state); }