FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(physics): add Vehicle softbody chassis by MasterLaplace · Pull Request #458 · EngineSquared/EngineSquared · GitHub

feat(physics): add Vehicle softbody chassis - #458

Closed
MasterLaplace wants to merge 12 commits into
mainfrom
vehicle-softbody-chassis
Closed

feat(physics): add Vehicle softbody chassis#458
MasterLaplace wants to merge 12 commits into
mainfrom
vehicle-softbody-chassis

Conversation

MasterLaplace commented Jan 31, 2026
edited by coderabbitai Bot
Loading

Copy link
Copy Markdown
Contributor

Not related to any issues

Summary by CodeRabbit

  • New Features
    • Deformable soft-body vehicle chassis component with presets (Default, Realistic, Arcade, Performance)
    • VehicleBuilder controls to enable/disable and configure soft-body chassis
    • Runtime soft-body lifecycle and systems: creation, per-frame skeleton sync, mesh sync, and shutdown (with fallback to rigid-body)
    • Mesh utilities: simplification, deduplication, and vertex-count estimation (optional normals handling)
    • Soft-body settings: new updatePosition toggle
    • New soft-body chassis diagnostic error type

✏️ Tip: You can customize this high-level summary in your review settings.

MasterLaplace self-assigned this Jan 31, 2026
MasterLaplace added the enhancement New feature or request label Jan 31, 2026

coderabbitai Bot commented Jan 31, 2026
edited
Loading

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds an Object::Utils mesh simplifier/deduper and a SoftBodyChassis subsystem: new soft-body component types and internals, builder hooks, plugin/system registration, Jolt soft-body creation/synchronization, and mesh processing flows (dedupe, simplify, vertex mapping).

Changes

Cohort / File(s) Summary
Mesh Simplifier
src/plugin/object/src/utils/MeshSimplifier.hpp, src/plugin/object/src/utils/MeshSimplifier.cpp
New clustering-based mesh simplification and deduplication utilities and API: SimplifyMesh, DeduplicateVertices, EstimateSimplifiedVertexCount, SimplificationSettings presets, SimplificationResult; spatial-hash + union-find clustering, centroid collapse, degenerate-face pruning, optional normals recompute, and vertex mapping.
ShapeGenerator include
src/plugin/object/src/utils/ShapeGenerator.hpp
Added includes for MeshSimplifier.hpp and MeshUtils.hpp; no logic changes.
Soft-Body Component API
src/plugin/physics/src/component/SoftBodyChassis.hpp, src/plugin/physics/src/component/SoftBodyChassisInternal.hpp, src/plugin/physics/src/component/SoftBody.hpp
New SoftBodyChassisSettings presets and SoftBodyChassis component; internal SoftBodyChassisInternal tracks skeleton/soft-body IDs, vertexMap, anchors, flags, and helpers; adds SoftBodySettings::updatePosition.
Soft-Body System
src/plugin/physics/src/system/SoftBodyChassisSystem.hpp, src/plugin/physics/src/system/SoftBodyChassisSystem.cpp
New system implementing lifecycle: dedupe/simplify meshes, create invisible skeleton RigidBody and Jolt soft body, build skinning constraints, store internal state, and per-frame Sync APIs to update skinning and copy physics vertex positions back to entity meshes.
Physics plugin integration
src/plugin/physics/src/plugin/PhysicsPlugin.cpp, src/plugin/physics/src/Physics.hpp
Registers soft-body chassis system init/shutdown and per-frame sync; added includes.
VehicleBuilder
src/plugin/physics/src/builder/VehicleBuilder.hpp
Adds SoftBodyChassis builder API (SetSoftBodyChassis, EnableSoftBodyChassis, DisableSoftBodyChassis, IsSoftBodyChassisEnabled) and conditional component creation when enabled.
SoftBody system refactor
src/plugin/physics/src/system/SoftBodySystem.cpp
Replaced local mesh deduplication with Object::Utils::DeduplicateVertices, adapted vertex/face mapping and propagation, and passed updatePosition into Jolt creation settings.
Exception
src/plugin/physics/src/exception/SoftBodyChassisError.hpp
New SoftBodyChassisError runtime exception type used by soft-body chassis creation/management.
Small includes
src/plugin/object/src/utils/ShapeGenerator.hpp, src/plugin/physics/src/Physics.hpp
Minor include additions to reference new headers.

Sequence Diagram(s)

sequenceDiagram
    participant Builder as VehicleBuilder
    participant Plugin as PhysicsPlugin
    participant System as SoftBodyChassisSystem
    participant ObjectUtils as Object::Utils
    participant Jolt as JoltPhysics
    participant Mesh as EntityMesh

    rect rgba(100,150,255,0.5)
        Builder->>Builder: EnableSoftBodyChassis()
        Builder->>Plugin: Build() with SoftBodyChassis component
        Plugin->>System: InitSoftBodyChassisSystem()
        System->>System: Register lifecycle hooks
    end

    rect rgba(100,200,100,0.5)
        Mesh->>System: Component attached (mesh + transform)
        System->>ObjectUtils: DeduplicateVertices(mesh)
        System->>ObjectUtils: SimplifyMesh(mesh, settings) [optional]
        System->>Jolt: Create skeleton RigidBody
        System->>Jolt: Create SoftBody with constraints
        System->>System: Store SoftBodyChassisInternal (vertexMap, IDs, state)
    end

    rect rgba(200,150,100,0.5)
        loop per-frame
            System->>Jolt: Read skeleton transforms
            System->>System: Skin vertices / update joint matrices
            System->>Jolt: Read SoftBody vertex positions
            System->>Mesh: Update vertex positions & recalc normals
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ripel2

Poem

🐰 I hopped through vertices, tidy and bright,
I clustered and merged till shapes held tight,
A silent skeleton hums beneath soft skin,
Vertices dance, normals gleam — the chassis wins.

🚥 Pre-merge checks | ✅ 2 | ❌ 1 ❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding soft body chassis support for vehicles in the physics plugin.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch vehicle-softbody-chassis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

MasterLaplace marked this pull request as ready for review January 31, 2026 12:02

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp`:
- Around line 421-425: The multiplication cellsX * cellsY * cellsZ can overflow
uint32_t; change the intermediate computation to use uint64_t (e.g., compute
uint64_t maxCells64 = static_cast<uint64_t>(cellsX) * cellsY * cellsZ), then
clamp or validate the result before assigning to uint32_t maxCells (use
std::numeric_limits<uint32_t>::max() or fail/resize if maxCells64 exceeds that)
and update any uses of maxCells accordingly to avoid silent wraparound in
MeshSimplifier.cpp (references: cellsX, cellsY, cellsZ, maxCells).
🧹 Nitpick comments (2)
src/plugin/object/src/utils/MeshSimplifier.cpp (2)

319-324: Missing vertexMap initialization when vertices is empty.

When vertices.empty() is true, result.vertexMap is not initialized, leaving it empty. While this is technically correct (no vertices to map), it's inconsistent with SimplifyMesh which always initializes vertexMap. Consider adding an explicit initialization for consistency.

♻️ Suggested improvement
     if (vertices.empty())
     {
         result.mesh = mesh;
         result.simplifiedVertexCount = 0;
         result.wasSimplified = false;
+        // vertexMap intentionally left empty for empty input
         return result;
     }

243-247: Texture coordinate averaging may produce incorrect results at UV seams.

When merging vertices at UV seams (where the same position has different texture coordinates), averaging the texture coordinates can produce incorrect results. This is a known limitation of vertex clustering algorithms.

Consider documenting this limitation or, for critical use cases, providing an option to preserve UV seam vertices.

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugin/physics/src/system/SoftBodySystem.cpp (1)

123-203: ⚠️ Potential issue | 🟠 Major

Remap softBody.edges through the dedup vertex map.

After deduplication, edge indices must be remapped to the deduped vertex indices or they can reference invalid/wrong vertices when no faces are present.

🛠️ Suggested fix
-    else if (!softBody.edges.empty())
+    else if (!softBody.edges.empty())
     {
         // For rope/chain without faces, use edge constraints from SoftBody
         settings->mEdgeConstraints.reserve(softBody.edges.size());
         for (const auto &[vertexA, vertexB] : softBody.edges)
         {
-            settings->mEdgeConstraints.emplace_back(
-                JPH::SoftBodySharedSettings::Edge(vertexA, vertexB, softBody.settings.edgeCompliance));
+            if (vertexA >= result.vertexMap.size() || vertexB >= result.vertexMap.size())
+            {
+                Log::Warn(fmt::format("SoftBody: Skipping edge with out-of-bounds indices ({}, {})", vertexA, vertexB));
+                continue;
+            }
+            uint32_t a = result.vertexMap[vertexA];
+            uint32_t b = result.vertexMap[vertexB];
+            if (a == b)
+                continue;
+            settings->mEdgeConstraints.emplace_back(
+                JPH::SoftBodySharedSettings::Edge(a, b, softBody.settings.edgeCompliance));
         }
     }
🤖 Fix all issues with AI agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp`:
- Around line 19-20: The SpatialHash constructor currently sets _invCellSize =
1.0f / cellSize which yields Inf for zero/negative inputs; update
SpatialHash(float cellSize) to validate or clamp cellSize (e.g., if cellSize <=
0.0f then set cellSize = max(cellSize, std::numeric_limits<float>::epsilon()) or
a small positive fallback) before assigning _cellSize and computing
_invCellSize, or alternatively assert/throw on invalid input; ensure GetKey and
any other methods relying on _invCellSize then operate on the validated/clamped
value.

In `@src/plugin/physics/src/system/SoftBodyChassisSystem.cpp`:
- Around line 452-469: The computation of invScale = 1.0f /
internal.initialScale can divide by zero if any component of initialScale is
zero; change it to clamp each component against a small epsilon (use the same
epsilon used in SoftBodySystem) before inversion (e.g., compute safeScale =
glm::max(internal.initialScale, glm::vec3(epsilon)) then invScale = 1.0f /
safeScale) so invScale contains no inf/NaN and the subsequent mesh.SetVertexAt
updates remain valid.
- Around line 223-303: The internal.vertexMap must map original mesh vertex
indices to the final Jolt vertex indices (deduped simplified mesh) — currently
when wasSimplified is true you assign only simplificationMap, losing the dedup
mapping from CreateChassisSharedSettings (vertexMap) and causing
incorrect/out-of-range indices during sync. Fix by composing the maps when
wasSimplified: take simplificationMap (orig->simplifiedIndex) and the dedup
vertexMap returned by CreateChassisSharedSettings (simplifiedIndex->joltIndex)
and build a combined map orig->joltIndex, then assign that combined map to
Component::SoftBodyChassisInternal::vertexMap (instead of assigning only
simplificationMap); keep the existing move semantics (use std::move where
appropriate) and ensure the code references simplificationMap, vertexMap (the
one returned by CreateChassisSharedSettings), internal.vertexMap and
wasSimplified.

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp`:
- Around line 281-368: The function SimplifyMesh currently unconditionally sets
result.wasSimplified = true at the end of the simplification path; change this
to reflect actual change by comparing counts: set result.wasSimplified =
(result.simplifiedVertexCount != result.originalVertexCount) ||
(newIndices.size() != indices.size()). Update the assignment for
result.wasSimplified (after computing result.simplifiedVertexCount and
newIndices) instead of the unconditional true so the flag accurately reflects
whether vertices or indices changed.

In `@src/plugin/physics/src/system/SoftBodyChassisSystem.cpp`:
- Around line 135-169: The static function CreateSkeletonBody is unused and
should be removed or explicitly documented; either delete the entire
CreateSkeletonBody function (including its references to JPH::BoxShapeSettings,
bodySettings, and physicsManager/GetBodyInterface/CreateBody) to eliminate dead
code, or keep it but add a clear comment above CreateSkeletonBody stating its
intended future use (e.g., purpose for VehicleConstraint skeleton bodies), why
it’s static/unused, and when/where it will be invoked so reviewers know it’s
intentionally retained.
🧹 Nitpick comments (1)
src/plugin/physics/src/system/SoftBodyChassisSystem.cpp (1)

42-51: Remove unused DeduplicatedMesh struct and orphaned doc comment.

The DeduplicatedMesh struct is defined but never used anywhere in this file. Additionally, the doc comment at line 49-51 appears to describe a function that was removed, leaving an orphaned comment block.

🧹 Suggested cleanup
-/**
- * `@brief` Structure holding deduplicated mesh data
- */
-struct DeduplicatedMesh {
-    std::vector<glm::vec3> vertices;
-    std::vector<uint32_t> indices;
-    std::vector<uint32_t> vertexMap;
-};
-
-/**
- * `@brief` Convert mesh to deduplicated indexed mesh
- */
-
 /**
  * `@brief` Create Jolt SoftBodySharedSettings with skinned constraints for chassis

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/plugin/physics/src/system/SoftBodyChassisSystem.cpp`:
- Around line 224-257: When autoSimplify is enabled but simplification didn't
reduce to settings.maxVertices and settings.fallbackToRigidBody is false,
explicitly handle that case instead of silently continuing with an over-budget
mesh: inside the branch where result.wasSimplified is false and
settings.fallbackToRigidBody is false, add a Log::Warn message describing that
simplification was insufficient and fallback is disabled, then if result.mesh is
available use it as the working mesh (assign workingMesh =
std::move(result.mesh); simplificationMap = std::move(result.vertexMap); set
wasSimplified accordingly) so we at least use the best-effort simplified mesh,
otherwise keep the original mesh but make the warning explicit and consider
setting a flag or metric to show the chassis remains over-budget; reference
symbols: meshVertices, settings.autoSimplify, settings.fallbackToRigidBody,
result, workingMesh, simplificationMap, wasSimplified, chassisComp.isActive.
🧹 Nitpick comments (1)
src/plugin/physics/src/system/SoftBodyChassisSystem.cpp (1)

42-51: Remove unused struct and orphan docstring.

The DeduplicatedMesh struct is defined but never used—the code uses Object::Utils::DeduplicateVertices which returns its own result type. The docstring at lines 49-51 has no associated function.

🧹 Suggested cleanup
-/**
- * `@brief` Structure holding deduplicated mesh data
- */
-struct DeduplicatedMesh {
-    std::vector<glm::vec3> vertices;
-    std::vector<uint32_t> indices;
-    std::vector<uint32_t> vertexMap;
-};
-
-/**
- * `@brief` Convert mesh to deduplicated indexed mesh
- */
-
 /**
  * `@brief` Create Jolt SoftBodySharedSettings with skinned constraints for chassis

Comment on lines +224 to +257
if (meshVertices.size() > settings.maxVertices)
{
if (settings.autoSimplify)
{
Log::Info(fmt::format("SoftBodyChassis: Simplifying mesh from {} to max {} vertices",
meshVertices.size(), settings.maxVertices));

auto simplifySettings = Object::Utils::SimplificationSettings::ForVehicleChassis();
simplifySettings.targetVertexCount = settings.maxVertices;

auto result = Object::Utils::SimplifyMesh(*mesh, simplifySettings);

if (result.wasSimplified && result.simplifiedVertexCount <= settings.maxVertices)
{
workingMesh = std::move(result.mesh);
simplificationMap = std::move(result.vertexMap);
wasSimplified = true;
Log::Info(fmt::format("SoftBodyChassis: Simplified to {} vertices", result.simplifiedVertexCount));
}
else if (settings.fallbackToRigidBody)
{
Log::Warn("SoftBodyChassis: Simplification insufficient, falling back to RigidBody");
chassisComp.isActive = false;
return;
}
}
else if (settings.fallbackToRigidBody)
{
Log::Warn(fmt::format("SoftBodyChassis: Mesh too complex ({} vertices), falling back to RigidBody",
meshVertices.size()));
chassisComp.isActive = false;
return;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

⚠️ Potential issue | 🟡 Minor

Simplification fallback logic allows over-budget mesh to proceed.

When autoSimplify is enabled, simplification fails to meet maxVertices, and fallbackToRigidBody is false, the code continues with the original over-budget mesh. This could cause performance degradation or unexpected behavior with Jolt.

Consider either logging a warning or providing explicit handling for this case:

💡 Suggested fix
                 if (result.wasSimplified && result.simplifiedVertexCount <= settings.maxVertices)
                 {
                     workingMesh = std::move(result.mesh);
                     simplificationMap = std::move(result.vertexMap);
                     wasSimplified = true;
                     Log::Info(fmt::format("SoftBodyChassis: Simplified to {} vertices", result.simplifiedVertexCount));
                 }
                 else if (settings.fallbackToRigidBody)
                 {
                     Log::Warn("SoftBodyChassis: Simplification insufficient, falling back to RigidBody");
                     chassisComp.isActive = false;
                     return;
                 }
+                else
+                {
+                    Log::Warn(fmt::format(
+                        "SoftBodyChassis: Simplification insufficient ({} > {} vertices), proceeding with over-budget mesh",
+                        result.simplifiedVertexCount, settings.maxVertices));
+                }
🤖 Prompt for AI Agents
In `@src/plugin/physics/src/system/SoftBodyChassisSystem.cpp` around lines 224 -
257, When autoSimplify is enabled but simplification didn't reduce to
settings.maxVertices and settings.fallbackToRigidBody is false, explicitly
handle that case instead of silently continuing with an over-budget mesh: inside
the branch where result.wasSimplified is false and settings.fallbackToRigidBody
is false, add a Log::Warn message describing that simplification was
insufficient and fallback is disabled, then if result.mesh is available use it
as the working mesh (assign workingMesh = std::move(result.mesh);
simplificationMap = std::move(result.vertexMap); set wasSimplified accordingly)
so we at least use the best-effort simplified mesh, otherwise keep the original
mesh but make the warning explicit and consider setting a flag or metric to show
the chassis remains over-budget; reference symbols: meshVertices,
settings.autoSimplify, settings.fallbackToRigidBody, result, workingMesh,
simplificationMap, wasSimplified, chassisComp.isActive.

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp`:
- Around line 263-270: The loop in MeshSimplifier.cpp that writes res.normals
from normalSums (inside the for over i) must guard against zero-length
normalSums before calling glm::normalize to avoid NaNs; update the block that
currently does res.normals[i] = glm::normalize(normalSums[i]) to compute the
squared length (or length) of normalSums[i], compare against a small epsilon,
and only normalize when above the threshold, otherwise set a safe fallback
normal (e.g., a unit up vector or the vertex-normal computed from surrounding
geometry) so res.normals[i] never becomes NaN; locate this change around the
variables normalSums, res.normals, clusterCounts and clusterSums in the same
loop.
- Around line 475-477: Clamp the computed cellSize to a small positive minimum
before using it to compute cellsX/cellsY to avoid division by zero when
settings.mergeDistance is zero or negative; specifically, replace the direct use
of settings.mergeDistance * 2.0f for cellSize with a clamped value (e.g.,
max(settings.mergeDistance * 2.0f, some small epsilon like 1e-6f)) and then use
that clamped cellSize when computing cellsX and cellsY so the ceil(extent.x /
cellSize) and ceil(extent.y / cellSize) calls cannot divide by zero.

Comment on lines +263 to +270
for (uint32_t i = 0; i < res.vertices.size(); ++i)
{
float invCount = 1.0f / static_cast<float>(clusterCounts[i]);
res.vertices[i] = clusterSums[i] * invCount;
if (!normals.empty())
{
res.normals[i] = glm::normalize(normalSums[i]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

⚠️ Potential issue | 🟡 Minor

Guard against zero-length normal sums before normalize.
If clustered normals cancel out, normalizing a near-zero vector can produce NaNs and leak into rendering. Add a length check/fallback.

🛠️ Proposed fix
-        if (!normals.empty())
-        {
-            res.normals[i] = glm::normalize(normalSums[i]);
-        }
+        if (!normals.empty())
+        {
+            float len = glm::length(normalSums[i]);
+            res.normals[i] = (len > 1e-8f) ? (normalSums[i] / len) : glm::vec3(0.0f);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (uint32_t i = 0; i < res.vertices.size(); ++i)
{
float invCount = 1.0f / static_cast<float>(clusterCounts[i]);
res.vertices[i] = clusterSums[i] * invCount;
if (!normals.empty())
{
res.normals[i] = glm::normalize(normalSums[i]);
}
for (uint32_t i = 0; i < res.vertices.size(); ++i)
{
float invCount = 1.0f / static_cast<float>(clusterCounts[i]);
res.vertices[i] = clusterSums[i] * invCount;
if (!normals.empty())
{
float len = glm::length(normalSums[i]);
res.normals[i] = (len > 1e-8f) ? (normalSums[i] / len) : glm::vec3(0.0f);
}
🤖 Prompt for AI Agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp` around lines 263 - 270, The
loop in MeshSimplifier.cpp that writes res.normals from normalSums (inside the
for over i) must guard against zero-length normalSums before calling
glm::normalize to avoid NaNs; update the block that currently does
res.normals[i] = glm::normalize(normalSums[i]) to compute the squared length (or
length) of normalSums[i], compare against a small epsilon, and only normalize
when above the threshold, otherwise set a safe fallback normal (e.g., a unit up
vector or the vertex-normal computed from surrounding geometry) so
res.normals[i] never becomes NaN; locate this change around the variables
normalSums, res.normals, clusterCounts and clusterSums in the same loop.

Comment on lines +475 to +477
float cellSize = settings.mergeDistance * 2.0f;
uint32_t cellsX = static_cast<uint32_t>(std::ceil(extent.x / cellSize)) + 1;
uint32_t cellsY = static_cast<uint32_t>(std::ceil(extent.y / cellSize)) + 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file to see if it exists and check the content around lines 475-477
ls -la src/plugin/object/src/utils/MeshSimplifier.cpp 2>/dev/null || echo "File not found at exact path, searching..."
fd "MeshSimplifier.cpp" --type f

Repository: EngineSquared/EngineSquared

Length of output: 214


🏁 Script executed:

# Read the specific lines mentioned in the review
sed -n '470,485p' src/plugin/object/src/utils/MeshSimplifier.cpp

Repository: EngineSquared/EngineSquared

Length of output: 858


🏁 Script executed:

# Check more context to understand how cellSize is used
sed -n '470,550p' src/plugin/object/src/utils/MeshSimplifier.cpp

Repository: EngineSquared/EngineSquared

Length of output: 928


🏁 Script executed:

# Search for mergeDistance in the file to understand constraints and how it's set
rg "mergeDistance" src/plugin/object/src/utils/MeshSimplifier.cpp -B 2 -A 2

Repository: EngineSquared/EngineSquared

Length of output: 1284


🏁 Script executed:

# Search for the settings structure definition
rg "struct.*settings|class.*settings" -i src/plugin/object/src/utils/MeshSimplifier.cpp -B 2 -A 10

Repository: EngineSquared/EngineSquared

Length of output: 53


🏁 Script executed:

# Search for where settings is initialized or validated
rg "mergeDistance\s*=" src/plugin/object/src/utils/MeshSimplifier.cpp -B 3 -A 1

Repository: EngineSquared/EngineSquared

Length of output: 223


🏁 Script executed:

# Look for the settings struct definition more broadly
fd "\.h$" --path "*plugin/object*" --type f | head -20 | xargs rg "mergeDistance" 2>/dev/null | head -20

Repository: EngineSquared/EngineSquared

Length of output: 1336


🏁 Script executed:

# Check the function signature that uses these settings
sed -n '450,480p' src/plugin/object/src/utils/MeshSimplifier.cpp

Repository: EngineSquared/EngineSquared

Length of output: 1141


🏁 Script executed:

# Look at the settings structure definition in the header file
cat src/plugin/object/src/utils/MeshSimplifier.hpp

Repository: EngineSquared/EngineSquared

Length of output: 6270


🏁 Script executed:

# Check if there's any validation of mergeDistance anywhere
rg "mergeDistance" src/plugin/object/src/utils/ -B 5 -A 5

Repository: EngineSquared/EngineSquared

Length of output: 7271


🏁 Script executed:

# Quick check for any callers or usage of EstimateSimplifiedVertexCount
rg "EstimateSimplifiedVertexCount" --type cpp --type hpp

Repository: EngineSquared/EngineSquared

Length of output: 98


🏁 Script executed:

# Search for EstimateSimplifiedVertexCount usage
rg "EstimateSimplifiedVertexCount"

Repository: EngineSquared/EngineSquared

Length of output: 354


Clamp cellSize to prevent division by zero or undefined behavior if settings.mergeDistance is zero or negative.

The divisions extent / cellSize on lines 476-478 will produce undefined behavior if cellSize ≤ 0, which can occur if settings.mergeDistance is zero or negative. While the default value is safe (0.01f), the struct lacks validation to prevent unsafe values from being set by callers.

Suggested fix
-    float cellSize = settings.mergeDistance * 2.0f;
+    float cellSize = std::max(settings.mergeDistance * 2.0f, 1e-6f);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
float cellSize = settings.mergeDistance * 2.0f;
uint32_t cellsX = static_cast<uint32_t>(std::ceil(extent.x / cellSize)) + 1;
uint32_t cellsY = static_cast<uint32_t>(std::ceil(extent.y / cellSize)) + 1;
float cellSize = std::max(settings.mergeDistance * 2.0f, 1e-6f);
uint32_t cellsX = static_cast<uint32_t>(std::ceil(extent.x / cellSize)) + 1;
uint32_t cellsY = static_cast<uint32_t>(std::ceil(extent.y / cellSize)) + 1;
🤖 Prompt for AI Agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp` around lines 475 - 477, Clamp
the computed cellSize to a small positive minimum before using it to compute
cellsX/cellsY to avoid division by zero when settings.mergeDistance is zero or
negative; specifically, replace the direct use of settings.mergeDistance * 2.0f
for cellSize with a clamped value (e.g., max(settings.mergeDistance * 2.0f, some
small epsilon like 1e-6f)) and then use that clamped cellSize when computing
cellsX and cellsY so the ceil(extent.x / cellSize) and ceil(extent.y / cellSize)
calls cannot divide by zero.

MasterLaplace and others added 10 commits February 1, 2026 22:55
…icle bodies

- Added SoftBodyChassis component to enable vehicle chassis deformation on collision.
- Introduced SoftBodyChassisInternal for managing Jolt references and skinning data.
- Created SoftBodyChassisSystem to handle creation, destruction, and synchronization of soft body chassis.
- Integrated mesh simplification for chassis to ensure performance and manage vertex count.
- Updated VehicleBuilder to support soft body chassis configuration.
- Enhanced SoftBody settings to include position update control during simulation.
- Added necessary includes and system registrations in PhysicsPlugin.
…ctors, unused numJoints member from constructors; clean up deduplication logic and improve mesh handling, streamline mesh deduplication and improve logging
…Body chassis management, catch specific SoftBodyChassisError for improved error logging during chassis construction, enhance spatial hashing and cluster collapsing logic for better performance and readability
…initialization and enhance vertex mapping logic for better clarity
…ine bounding box computation, and improve merge distance logic for clarity and performance
MasterLaplace force-pushed the vehicle-softbody-chassis branch from 4015fc4 to c619c83 Compare February 1, 2026 21:55

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/plugin/physics/src/component/SoftBodyChassisInternal.hpp`:
- Around line 58-64: The field skeletonBodyID in SoftBodyChassisInternal is left
uninitialized causing HasValidSkeleton() to be unreliable; fix it by
initializing skeletonBodyID to a known invalid value (e.g., JPH::BodyID() or
JPH::BodyID::sInvalid) in SoftBodyChassisInternal's constructor OR ensure the
code that creates a SoftBodyChassisInternal instance in SoftBodyChassisSystem
assigns the actual created rigid body ID to skeletonBodyID after creation;
update either the SoftBodyChassisInternal constructor or the instantiation site
so skeletonBodyID is deterministically set before any call to
HasValidSkeleton().
🧹 Nitpick comments (3)
src/plugin/object/src/utils/MeshSimplifier.hpp (1)

150-153: Consider using uint32_t for consistency.

The function compares mesh.GetVertices().size() (which returns size_t) against uint32_t maxVertices. This works but may trigger compiler warnings on some platforms due to signed/unsigned comparison.

🔧 Optional: Explicit cast for clarity
 [[nodiscard]] inline bool NeedsSimplification(const Component::Mesh &mesh, uint32_t maxVertices)
 {
-    return mesh.GetVertices().size() > maxVertices;
+    return static_cast<uint32_t>(mesh.GetVertices().size()) > maxVertices;
 }
src/plugin/physics/src/builder/VehicleBuilder.hpp (1)

22-22: Remove unused <optional> include.

The <optional> header is included but not used. _softBodyChassisSettings is stored by value, not as std::optional.

🧹 Remove unused include
-#include <optional>
src/plugin/physics/src/system/SoftBodyChassisSystem.cpp (1)

42-46: Remove unused DeduplicatedMesh struct.

This struct is defined but never used. The code at line 66 uses Object::Utils::DeduplicateVertices which returns a SimplificationResult, not this struct.

🧹 Remove dead code
-/**
- * `@brief` Structure holding deduplicated mesh data
- */
-struct DeduplicatedMesh {
-    std::vector<glm::vec3> vertices;
-    std::vector<uint32_t> indices;
-    std::vector<uint32_t> vertexMap;
-};
-
-/**
- * `@brief` Convert mesh to deduplicated indexed mesh
- */

MasterLaplace and others added 2 commits February 2, 2026 00:47
…ization, streamline chassis shared settings creation, and improve soft body synchronization logic

sonarqubecloud Bot commented Feb 1, 2026

Copy link
Copy Markdown

coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/plugin/physics/src/component/SoftBodyChassisInternal.hpp`:
- Around line 149-161: The IsValid() check always fails because skeletonBodyID
is never set; update the code to remove the unused skeleton requirement by
deleting HasValidSkeleton() and skeletonBodyID usages and change IsValid() (in
SoftBodyChassisInternal) to return HasValidSoftBody() && isInitialized; then
update any call sites (e.g., SyncSoftBodyChassisWithSkeleton loop in
SoftBodyChassisSystem.cpp) that reference HasValidSkeleton or skeletonBodyID to
rely on the new IsValid() semantics and ensure anchorsInitialized and softBodyID
usage remain intact so processing continues when the soft body is present and
initialized.
🧹 Nitpick comments (2)
src/plugin/physics/src/system/SoftBodyChassisSystem.cpp (2)

42-46: Remove unused DeduplicatedMesh struct.

This struct is defined but never used. The actual deduplication at line 85 uses Object::Utils::DeduplicateVertices() which returns a SimplificationResult type, not this struct.

♻️ Suggested removal
-/**
- * `@brief` Structure holding deduplicated mesh data
- */
-struct DeduplicatedMesh {
-    std::vector<glm::vec3> vertices;
-    std::vector<uint32_t> indices;
-    std::vector<uint32_t> vertexMap;
-};
-
-/**
- * `@brief` Convert mesh to deduplicated indexed mesh
- */

413-424: Redundant if/else branches with identical logic.

Both branches execute the same SetPositionAndRotationInternal call. The only difference is setting hardSkinNextFrame = false. This can be simplified:

♻️ Suggested simplification
         // On first frame or reset, teleport the entire soft body to match chassis
-        if (internal.hardSkinNextFrame)
-        {
-            // Set the body position and rotation to match chassis
-            body.SetPositionAndRotationInternal(Utils::ToJoltRVec3(position), Utils::ToJoltQuat(rotation));
-            internal.hardSkinNextFrame = false;
-        }
-        else
-        {
-            // Normal frame: Update position/rotation
-            body.SetPositionAndRotationInternal(Utils::ToJoltRVec3(position), Utils::ToJoltQuat(rotation));
-        }
+        // Update soft body position and rotation to match chassis
+        body.SetPositionAndRotationInternal(Utils::ToJoltRVec3(position), Utils::ToJoltQuat(rotation));
+        internal.hardSkinNextFrame = false;

If the hardSkinNextFrame case was intended to have different behavior (e.g., also teleporting vertices directly), that logic should be added.

Comment on lines +149 to +161
[[nodiscard]] bool HasValidSkeleton() const { return !skeletonBodyID.IsInvalid(); }

/**
* @brief Check if soft body ID is valid
* @return true if soft body ID is valid
*/
[[nodiscard]] bool HasValidSoftBody() const { return !softBodyID.IsInvalid(); }

/**
* @brief Check if both bodies are valid and system is ready
* @return true if fully initialized
*/
[[nodiscard]] bool IsValid() const { return HasValidSkeleton() && HasValidSoftBody() && isInitialized; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

⚠️ Potential issue | 🔴 Critical

IsValid() will always return false, breaking skeleton sync.

IsValid() requires HasValidSkeleton() to be true, but skeletonBodyID is never assigned a valid value in SoftBodyChassisSystem.cpp (only softBodyID is set at line 276). This causes SyncSoftBodyChassisWithSkeleton at line 395 to always skip processing:

if (!chassis.isActive || !internal.IsValid() || !internal.anchorsInitialized)
    continue;

Either:

  1. Remove skeletonBodyID and HasValidSkeleton() if the skeleton body concept is abandoned, and update IsValid() accordingly
  2. Or actually create and assign the skeleton body in the system
🛠️ Suggested fix (option 1: remove skeleton requirement)
-    [[nodiscard]] bool IsValid() const { return HasValidSkeleton() && HasValidSoftBody() && isInitialized; }
+    [[nodiscard]] bool IsValid() const { return HasValidSoftBody() && isInitialized; }
🤖 Prompt for AI Agents
In `@src/plugin/physics/src/component/SoftBodyChassisInternal.hpp` around lines
149 - 161, The IsValid() check always fails because skeletonBodyID is never set;
update the code to remove the unused skeleton requirement by deleting
HasValidSkeleton() and skeletonBodyID usages and change IsValid() (in
SoftBodyChassisInternal) to return HasValidSoftBody() && isInitialized; then
update any call sites (e.g., SyncSoftBodyChassisWithSkeleton loop in
SoftBodyChassisSystem.cpp) that reference HasValidSkeleton or skeletonBodyID to
rely on the new IsValid() semantics and ensure anchorsInitialized and softBodyID
usage remain intact so processing continues when the soft body is present and
initialized.

Miou-zora closed this Feb 10, 2026
Miou-zora deleted the vehicle-softbody-chassis branch March 23, 2026 12:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants


Back | FazBrowse Home | New Git URL