Skip to content

Comments

feat: add Softbody attachement and mesh simplifier#476

Open
MasterLaplace wants to merge 3 commits intomainfrom
softbody-attachement-and-mesh-simplifier
Open

feat: add Softbody attachement and mesh simplifier#476
MasterLaplace wants to merge 3 commits intomainfrom
softbody-attachement-and-mesh-simplifier

Conversation

@MasterLaplace
Copy link
Contributor

@MasterLaplace MasterLaplace commented Feb 15, 2026

Unrelated to any issue

Summary by CodeRabbit

Release Notes

  • New Features

    • Mesh simplification tools with multiple strategies: targeted vertex reduction, voxel-based simplification, and deduplication.
    • Soft body attachment support enabling soft bodies to be attached to rigid bodies with configurable kinematic vertices.
  • Chores

    • Enhanced documentation configuration with improved styling, MathJax 3 support, and expanded input sources.

@MasterLaplace MasterLaplace self-assigned this Feb 15, 2026
@MasterLaplace MasterLaplace added the enhancement New feature or request label Feb 15, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

Warning

Rate limit exceeded

@MasterLaplace has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR introduces mesh simplification utilities with spatial hashing and vertex clustering algorithms, extends soft body physics to support attachment to rigid bodies with kinematic vertex control, adds a new DEBRIS physics layer for attached soft bodies, and updates documentation generation configuration with enhanced Doxygen settings.

Changes

Cohort / File(s) Summary
Documentation Configuration
.github/workflows/deploy_doxygen_page.yml, Doxyfile.cfg
Updated Doxygen workflow triggers to use workflow\_dispatch and push filters; upgraded checkout action to v5, theme to v2.4.1, and renamed Doxyfile to Doxyfile.cfg. Enhanced Doxyfile configuration with PROJECT\_NUMBER 0.2.0, expanded INPUT sources, added MathJax 3 support, enabled man page generation, and activated HTML dynamic sections and call/caller graphs.
Mesh Simplification Utilities
src/plugin/object/src/Object.hpp, src/plugin/object/src/utils/MeshSimplifier.hpp, src/plugin/object/src/utils/MeshSimplifier.cpp
Introduced comprehensive mesh simplification module with spatial hashing, union-find clustering, and three simplification strategies: iterative merge-distance-based, target vertex count, and voxel-based. Includes deduplication and normal recalculation; exports SimplifyMesh, SimplifyMeshToTarget, SimplifyMeshByVoxelization, DeduplicateVertices, and EstimateSimplifiedVertexCount functions.
Soft Body Attachment and Kinematic Vertices
src/plugin/physics/src/component/SoftBody.hpp, src/plugin/physics/src/component/SoftBodyInternal.hpp, src/plugin/physics/src/system/SoftBodySystem.hpp, src/plugin/physics/src/system/SoftBodySystem.cpp
Extended SoftBody component with optional attachment to rigid bodies and kinematic vertex support (percentage-based or explicit indices). Enhanced SoftBodySystem to handle mesh derivation from colliders, compute kinematic vertex positions, and implement UpdateKinematicVertices system to synchronize attached soft bodies with parent transforms.
Physics Layer System
src/plugin/physics/src/utils/Layers.hpp, src/plugin/physics/src/utils/BroadPhaseLayers.hpp, src/plugin/physics/src/utils/BroadPhaseLayerImpl.hpp, src/plugin/physics/src/utils/ObjectLayerPairFilterImpl.hpp, src/plugin/physics/src/utils/ObjectVsBroadPhaseLayerFilterImpl.hpp
Added new DEBRIS layer (value 2) for attached soft bodies; updated NUM\_LAYERS to 3. Modified collision policies: NON\_MOVING now collides with MOVING or DEBRIS; MOVING excludes DEBRIS; DEBRIS only collides with NON\_MOVING.
Physics Plugin Registration
src/plugin/physics/src/plugin/PhysicsPlugin.cpp
Registered UpdateKinematicVertices as a FixedTimeUpdate system alongside existing physics systems.

Sequence Diagram

sequenceDiagram
    participant Editor as Editor/Game
    participant PhysicsPlugin as PhysicsPlugin
    participant SoftBodySystem as SoftBodySystem
    participant Scheduler as FixedTimeUpdate Scheduler
    participant RigidBody as RigidBody
    participant SoftBody as SoftBody Physics

    Editor->>PhysicsPlugin: Initialize soft body with attachedEntityId
    PhysicsPlugin->>SoftBodySystem: Create soft body (with attachment)
    SoftBodySystem->>SoftBodySystem: SelectKinematicVerticesByY()
    SoftBodySystem->>SoftBody: Store kinematicVertexIndices & localPositions
    
    loop Each Frame (FixedTimeUpdate)
        Scheduler->>SoftBodySystem: UpdateKinematicVertices()
        SoftBodySystem->>RigidBody: Get parent transform
        SoftBodySystem->>SoftBodySystem: Compute world positions from local
        SoftBodySystem->>SoftBody: Update kinematic vertex positions
        Scheduler->>SoftBodySystem: SyncSoftBodyVertices()
        SoftBodySystem->>SoftBody: Apply vertex positions to physics
        Scheduler->>SoftBodySystem: Regular physics update
        SoftBody->>RigidBody: Kinematic vertices follow parent
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • ripel2

Poem

🐰 A simplifier hops through vertex clouds,
With spatial hashing, clustering proud,
Soft bodies now attach with care,
Kinematic vertices float through air,
Physics layers sorted fair! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main
Title check ✅ Passed The title 'feat: add Softbody attachement and mesh simplifier' directly summarizes the two main features introduced in the PR: softbody attachment functionality and mesh simplification utilities.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch softbody-attachement-and-mesh-simplifier

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.

- Introduced MeshSimplifier.hpp for mesh simplification, including settings and methods for reducing vertex count while preserving shape.
- Enhanced SoftBody component to support attachment to rigid bodies, allowing kinematic vertices to follow parent transforms.
- Implemented UpdateKinematicVertices system to update positions of kinematic vertices based on parent body transforms.
- Updated physics plugin to register new systems for kinematic vertex updates.
- Modified soft body system to handle kinematic vertex selection and attachment logic.
- Improved broad phase layer handling to include a new DEBRIS layer for soft bodies.
- Adjusted collision filtering to ensure proper interactions between moving objects and debris.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
Doxyfile.cfg (1)

2672-2684: ⚠️ Potential issue | 🟡 Minor

CALL_GRAPH and CALLER_GRAPH are ineffective with HAVE_DOT = NO.

CALL_GRAPH = YES (Line 2672) and CALLER_GRAPH = YES (Line 2684) both require HAVE_DOT = YES (currently NO at Line 2500) and the dot tool (from Graphviz) to be installed. With HAVE_DOT = NO, these settings are silently ignored and no call/caller graphs will be generated.

Either set HAVE_DOT = YES and install Graphviz in the CI workflow, or set these graph tags to NO to avoid confusion.

🤖 Fix all issues with AI agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp`:
- Around line 497-509: The loop in MeshSimplifier.cpp that computes cell
coordinates for each vertex (using variables cx, cy, cz in the for loop
iterating over vertices) casts floor(...) results to uint32_t which can UB if
the float is slightly negative; modify the computation to avoid casting negative
floats by clamping the float result to >= 0.0f before static_cast (or switch to
signed ints and clamp after), and apply the same fix to the identical pattern
later in the file (the block around the second occurrence at the lines mentioned
in the review). Ensure you still clamp to cellsX-1 / cellsY-1 / cellsZ-1 after
conversion.
- Around line 391-428: The SimplificationResult bestResult is returned
uninitialized if maxIterations == 0; update the function to either early-handle
the zero-iteration case or fully initialize bestResult before the loop: set
bestResult.originalVertexCount to mesh.GetVertices().size(), set
bestResult.simplifiedVertexCount to that same value (or to 0/appropriate
default), set bestResult.wasSimplified to false, and initialize
bestResult.vertexMap to an empty map/vector. Ensure these initializations occur
before the for-loop (or return a valid result immediately when maxIterations ==
0) so callers of SimplifyMesh/SimplificationResult get a well-defined object.

In `@src/plugin/object/src/utils/MeshSimplifier.hpp`:
- Around line 68-69: The field preserveBoundaries on MeshSimplifier is declared
but never used; either remove it or implement boundary-preservation checks in
the simplification routines. Locate the MeshSimplifier class and either delete
the preserveBoundaries member and any builder/exposure methods, or thread its
value into the simplifier logic: check preserveBoundaries inside SimplifyMesh,
SimplifyMeshToTarget, SimplifyMeshByVoxelization and DeduplicateVertices and
modify edge/collapse selection to skip collapsing or altering edges that are
boundary edges (edges with only one adjacent face). Ensure the builder methods
remain consistent with the chosen approach (remove builder when removing the
field, or keep and honor the flag when implementing).

In `@src/plugin/physics/src/component/SoftBody.hpp`:
- Around line 272-275: The comment on SoftBody::attachedEntityId references a
nonexistent AttachToEntity() method; update the comment or add the method.
Either implement a SoftBody::AttachToEntity(Engine::EntityId) method (or
similarly named attach function) that sets attachedEntityId and documents
intended behavior, or change the inline comment for attachedEntityId to refer to
the actual API used to attach (e.g., set attachedEntityId directly or call the
existing attach function name) so the doc matches the code.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp`:
- Around line 709-727: The per-tick Log::Warn calls inside the
SyncSoftBodyVertices loop (the block that checks ConvexHullMeshCollider and
MeshCollider and currently calls Log::Warn "SoftBody: Using ... mesh as geometry
for SoftBody" or "SoftBody: No Mesh component found...") will spam logs; either
change these Log::Warn calls to Log::Debug, or add a one-time flag (e.g., a bool
on the SoftBody component or an internal map keyed by entity) that records that
the warning has been emitted and only logs on the first occurrence, setting the
flag after emitting; update the calls referring to ConvexHullMeshCollider,
MeshCollider and the "No Mesh" path accordingly and ensure the continue behavior
remains unchanged.
- Around line 58-100: The implementation of SelectKinematicVerticesByY does not
match its name/docs — it selects vertices via a uniform stride instead of by
lowest Y; either restore Y-based selection or rename and update docs. To fix: if
you want Y-based behavior, sort or index the input vertices by glm::vec3.y
(preserving original indices), select the lowest N indices (compute count using
std::ceil with std::clamp like currently done) and return those indices; update
variables like kinematicIndices accordingly and keep the 0%/100% short-circuits.
Alternatively, if you prefer uniform selection, rename
SelectKinematicVerticesByY to SelectKinematicVerticesUniform (and update the
function comment to describe uniform/stride selection) and adjust any callers to
use the new name.
- Around line 494-514: The code reads softBody.attachedEntityId.value() and
calls registry.try_get<...>(attachedEntity) without verifying the entity exists;
update the SoftBodySystem code to first check registry.valid(attachedEntity) (or
equivalent) after obtaining attachedEntity and before any try_get calls, and if
the entity is not valid log a warning (similar to the existing message) and skip
the attachment logic so attachedBodyID is not set from an invalid entity.
- Line 842: Remove the unused local variable by deleting the line that assigns
auto &bodyInterface = physicsManager.GetBodyInterface(); inside
UpdateKinematicVertices; if any later code intended to use the BodyInterface,
instead access it where needed via physicsManager.GetBodyInterface() inline or
introduce a used variable with a clear name, but otherwise remove the unused
declaration to eliminate the warning.
- Around line 858-871: Update the lock ordering to consistently acquire the
parent body lock before the soft-body lock to avoid ABBA inversion between
UpdateKinematicVertices and SyncSoftBodyVertices; in SyncSoftBodyVertices
reorder the BodyLockRead for the parent (using internal.attachedToBodyID) to be
taken before the soft-body BodyLockRead (using internal.bodyID), preserve the
Succeeded checks and early-continue behavior, and ensure you keep the same lock
types (read vs read) and variable names so the locking semantics match
UpdateKinematicVertices exactly.
🧹 Nitpick comments (3)
.github/workflows/deploy_doxygen_page.yml (1)

4-8: Docs won't auto-regenerate on source changes.

The workflow only triggers on changes to the workflow file itself and Doxyfile.cfg. Source code changes under src/ won't trigger a rebuild. If this is intentional (relying on workflow_dispatch for manual runs), consider adding a brief comment to clarify.

src/plugin/object/src/utils/MeshSimplifier.hpp (1)

29-29: Unused <optional> include.

The <optional> header is included but nothing in this header uses std::optional. Consider removing it to avoid unnecessary compile-time dependencies.

src/plugin/physics/src/system/SoftBodySystem.cpp (1)

146-155: Weak hash combiner may cause excessive collisions on large meshes.

The current h1 ^ (h2 << 1) ^ (h3 << 2) has poor bit mixing — small shifts cause many collisions for vertices with similar coordinates. A common improvement:

♻️ Suggested hash combiner
     size_t operator()(const glm::vec3 &v) const
     {
-        // Use a simple hash combining x, y, z components
         size_t h1 = std::hash<float>{}(v.x);
         size_t h2 = std::hash<float>{}(v.y);
         size_t h3 = std::hash<float>{}(v.z);
-        return h1 ^ (h2 << 1) ^ (h3 << 2);
+        // boost::hash_combine style mixing
+        size_t seed = h1;
+        seed ^= h2 + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+        seed ^= h3 + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+        return seed;
     }

Comment on lines +391 to +428
// Start with base settings
SimplificationSettings settings = SimplificationSettings::ForSoftBody(targetVertexCount);
Component::Mesh currentMesh = mesh;
SimplificationResult bestResult;
bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());

for (uint32_t iter = 0; iter < maxIterations; ++iter)
{
auto result = SimplifyMesh(currentMesh, settings);

if (result.simplifiedVertexCount <= targetVertexCount)
{
// Target reached
result.originalVertexCount = bestResult.originalVertexCount;
return result;
}

// Not enough reduction, increase aggressiveness
if (result.simplifiedVertexCount >= currentMesh.GetVertices().size() * 0.95f)
{
// No significant progress, increase merge distance more aggressively
settings.mergeDistance *= 2.0f;
settings.aggressiveness = std::min(settings.aggressiveness + 0.15f, 1.0f);
}
else
{
// Some progress, moderate increase
settings.mergeDistance *= 1.5f;
settings.aggressiveness = std::min(settings.aggressiveness + 0.1f, 1.0f);
}

currentMesh = result.mesh;
bestResult = result;
bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());
}

return bestResult;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Uninitialized bestResult fields returned when maxIterations == 0.

If maxIterations is 0, the loop never executes and bestResult is returned at Line 427 with simplifiedVertexCount, wasSimplified, and vertexMap all uninitialized/default. The static analysis tool also flagged this (uninitvar).

Initialize bestResult fully or handle the maxIterations == 0 edge case explicitly.

Proposed fix
     SimplificationResult bestResult;
     bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());
+    bestResult.simplifiedVertexCount = bestResult.originalVertexCount;
+    bestResult.wasSimplified = false;
+    bestResult.mesh = mesh;
+    bestResult.vertexMap.resize(bestResult.originalVertexCount);
+    for (uint32_t i = 0; i < bestResult.originalVertexCount; ++i)
+        bestResult.vertexMap[i] = i;
📝 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
// Start with base settings
SimplificationSettings settings = SimplificationSettings::ForSoftBody(targetVertexCount);
Component::Mesh currentMesh = mesh;
SimplificationResult bestResult;
bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());
for (uint32_t iter = 0; iter < maxIterations; ++iter)
{
auto result = SimplifyMesh(currentMesh, settings);
if (result.simplifiedVertexCount <= targetVertexCount)
{
// Target reached
result.originalVertexCount = bestResult.originalVertexCount;
return result;
}
// Not enough reduction, increase aggressiveness
if (result.simplifiedVertexCount >= currentMesh.GetVertices().size() * 0.95f)
{
// No significant progress, increase merge distance more aggressively
settings.mergeDistance *= 2.0f;
settings.aggressiveness = std::min(settings.aggressiveness + 0.15f, 1.0f);
}
else
{
// Some progress, moderate increase
settings.mergeDistance *= 1.5f;
settings.aggressiveness = std::min(settings.aggressiveness + 0.1f, 1.0f);
}
currentMesh = result.mesh;
bestResult = result;
bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());
}
return bestResult;
}
// Start with base settings
SimplificationSettings settings = SimplificationSettings::ForSoftBody(targetVertexCount);
Component::Mesh currentMesh = mesh;
SimplificationResult bestResult;
bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());
bestResult.simplifiedVertexCount = bestResult.originalVertexCount;
bestResult.wasSimplified = false;
bestResult.mesh = mesh;
bestResult.vertexMap.resize(bestResult.originalVertexCount);
for (uint32_t i = 0; i < bestResult.originalVertexCount; ++i)
bestResult.vertexMap[i] = i;
for (uint32_t iter = 0; iter < maxIterations; ++iter)
{
auto result = SimplifyMesh(currentMesh, settings);
if (result.simplifiedVertexCount <= targetVertexCount)
{
// Target reached
result.originalVertexCount = bestResult.originalVertexCount;
return result;
}
// Not enough reduction, increase aggressiveness
if (result.simplifiedVertexCount >= currentMesh.GetVertices().size() * 0.95f)
{
// No significant progress, increase merge distance more aggressively
settings.mergeDistance *= 2.0f;
settings.aggressiveness = std::min(settings.aggressiveness + 0.15f, 1.0f);
}
else
{
// Some progress, moderate increase
settings.mergeDistance *= 1.5f;
settings.aggressiveness = std::min(settings.aggressiveness + 0.1f, 1.0f);
}
currentMesh = result.mesh;
bestResult = result;
bestResult.originalVertexCount = static_cast<uint32_t>(mesh.GetVertices().size());
}
return bestResult;
}
🧰 Tools
🪛 Cppcheck (2.19.0)

[warning] 427-427: Uninitialized variables

(uninitvar)

🤖 Prompt for AI Agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp` around lines 391 - 428, The
SimplificationResult bestResult is returned uninitialized if maxIterations == 0;
update the function to either early-handle the zero-iteration case or fully
initialize bestResult before the loop: set bestResult.originalVertexCount to
mesh.GetVertices().size(), set bestResult.simplifiedVertexCount to that same
value (or to 0/appropriate default), set bestResult.wasSimplified to false, and
initialize bestResult.vertexMap to an empty map/vector. Ensure these
initializations occur before the for-loop (or return a valid result immediately
when maxIterations == 0) so callers of SimplifyMesh/SimplificationResult get a
well-defined object.

Comment on lines +497 to +509
for (uint32_t i = 0; i < vertices.size(); ++i)
{
const glm::vec3 &v = vertices[i];

// Calculate cell coordinates
uint32_t cx = static_cast<uint32_t>(std::floor((v.x - minBound.x) * invCellSize));
uint32_t cy = static_cast<uint32_t>(std::floor((v.y - minBound.y) * invCellSize));
uint32_t cz = static_cast<uint32_t>(std::floor((v.z - minBound.z) * invCellSize));

// Clamp to grid bounds
cx = std::min(cx, cellsX - 1);
cy = std::min(cy, cellsY - 1);
cz = std::min(cz, cellsZ - 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential undefined behavior: casting negative float to uint32_t.

If floating-point imprecision causes (v.x - minBound.x) to be slightly negative, std::floor yields -1.0f, and static_cast<uint32_t>(-1.0f) is undefined behavior in C++. The std::min clamp on Lines 507-509 happens after the cast, so it doesn't prevent this.

Clamp to 0.0f before the cast, or use signed integers and clamp afterward.

Proposed fix
-        uint32_t cx = static_cast<uint32_t>(std::floor((v.x - minBound.x) * invCellSize));
-        uint32_t cy = static_cast<uint32_t>(std::floor((v.y - minBound.y) * invCellSize));
-        uint32_t cz = static_cast<uint32_t>(std::floor((v.z - minBound.z) * invCellSize));
-
-        // Clamp to grid bounds
-        cx = std::min(cx, cellsX - 1);
-        cy = std::min(cy, cellsY - 1);
-        cz = std::min(cz, cellsZ - 1);
+        uint32_t cx = static_cast<uint32_t>(std::clamp(std::floor((v.x - minBound.x) * invCellSize), 0.0f, static_cast<float>(cellsX - 1)));
+        uint32_t cy = static_cast<uint32_t>(std::clamp(std::floor((v.y - minBound.y) * invCellSize), 0.0f, static_cast<float>(cellsY - 1)));
+        uint32_t cz = static_cast<uint32_t>(std::clamp(std::floor((v.z - minBound.z) * invCellSize), 0.0f, static_cast<float>(cellsZ - 1)));

Note: the same pattern appears on Lines 550-555 — apply the same fix there.

📝 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 < vertices.size(); ++i)
{
const glm::vec3 &v = vertices[i];
// Calculate cell coordinates
uint32_t cx = static_cast<uint32_t>(std::floor((v.x - minBound.x) * invCellSize));
uint32_t cy = static_cast<uint32_t>(std::floor((v.y - minBound.y) * invCellSize));
uint32_t cz = static_cast<uint32_t>(std::floor((v.z - minBound.z) * invCellSize));
// Clamp to grid bounds
cx = std::min(cx, cellsX - 1);
cy = std::min(cy, cellsY - 1);
cz = std::min(cz, cellsZ - 1);
for (uint32_t i = 0; i < vertices.size(); ++i)
{
const glm::vec3 &v = vertices[i];
// Calculate cell coordinates
uint32_t cx = static_cast<uint32_t>(std::clamp(std::floor((v.x - minBound.x) * invCellSize), 0.0f, static_cast<float>(cellsX - 1)));
uint32_t cy = static_cast<uint32_t>(std::clamp(std::floor((v.y - minBound.y) * invCellSize), 0.0f, static_cast<float>(cellsY - 1)));
uint32_t cz = static_cast<uint32_t>(std::clamp(std::floor((v.z - minBound.z) * invCellSize), 0.0f, static_cast<float>(cellsZ - 1)));
🤖 Prompt for AI Agents
In `@src/plugin/object/src/utils/MeshSimplifier.cpp` around lines 497 - 509, The
loop in MeshSimplifier.cpp that computes cell coordinates for each vertex (using
variables cx, cy, cz in the for loop iterating over vertices) casts floor(...)
results to uint32_t which can UB if the float is slightly negative; modify the
computation to avoid casting negative floats by clamping the float result to >=
0.0f before static_cast (or switch to signed ints and clamp after), and apply
the same fix to the identical pattern later in the file (the block around the
second occurrence at the lines mentioned in the review). Ensure you still clamp
to cellsX-1 / cellsY-1 / cellsZ-1 after conversion.

Comment on lines +68 to +69
/// Whether to preserve boundary edges (edges with only one adjacent face)
bool preserveBoundaries = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'preserveBoundaries' --type=cpp --type-add 'cpp:*.hpp'

Repository: EngineSquared/EngineSquared

Length of output: 332


🏁 Script executed:

#!/bin/bash
# Get context around the lines where preserveBoundaries appears
head -n 110 src/plugin/object/src/utils/MeshSimplifier.hpp | tail -n 50

# Search for conditional usage of preserveBoundaries (if statements, ternary, etc.)
echo "=== Searching for conditional checks on preserveBoundaries ==="
rg -n 'if.*preserveBoundaries|preserveBoundaries.*if|?.*preserveBoundaries' --type=cpp --type-add 'cpp:*.hpp'

# Also check for any .cpp file
echo "=== Checking for MeshSimplifier.cpp ==="
find . -name 'MeshSimplifier.cpp' -type f

Repository: EngineSquared/EngineSquared

Length of output: 1983


🏁 Script executed:

#!/bin/bash
# Search for any reference to preserveBoundaries in the cpp file
echo "=== All references in MeshSimplifier.cpp ==="
rg -n 'preserveBoundaries' src/plugin/object/src/utils/MeshSimplifier.cpp

# Search for if/conditional patterns checking the field
echo "=== Searching for conditional checks ==="
rg -n 'if\s*\(' src/plugin/object/src/utils/MeshSimplifier.cpp -A 2 | grep -C 2 'preserveBoundaries'

# Get a broader view of the cpp file to understand what operations it performs
echo "=== File size and structure ==="
wc -l src/plugin/object/src/utils/MeshSimplifier.cpp

Repository: EngineSquared/EngineSquared

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Read the entire cpp file to see what's actually implemented
cat -n src/plugin/object/src/utils/MeshSimplifier.cpp | head -n 150

Repository: EngineSquared/EngineSquared

Length of output: 5442


🏁 Script executed:

#!/bin/bash
# Continue reading the cpp file, looking for the main Simplify function
sed -n '150,400p' src/plugin/object/src/utils/MeshSimplifier.cpp | cat -n

Repository: EngineSquared/EngineSquared

Length of output: 10178


🏁 Script executed:

#!/bin/bash
# Read the rest of the cpp file
tail -n +400 src/plugin/object/src/utils/MeshSimplifier.cpp | cat -n

Repository: EngineSquared/EngineSquared

Length of output: 13393


preserveBoundaries setting is unused and should be removed or implemented.

The field is declared with intent to preserve boundary edges (edges with only one adjacent face), exposed through builder methods, but never actually checked in any of the simplification implementations (SimplifyMesh, SimplifyMeshToTarget, SimplifyMeshByVoxelization, or DeduplicateVertices). Either implement the boundary preservation logic in the mesh simplification algorithm or remove the field to prevent misleading callers about available functionality.

🤖 Prompt for AI Agents
In `@src/plugin/object/src/utils/MeshSimplifier.hpp` around lines 68 - 69, The
field preserveBoundaries on MeshSimplifier is declared but never used; either
remove it or implement boundary-preservation checks in the simplification
routines. Locate the MeshSimplifier class and either delete the
preserveBoundaries member and any builder/exposure methods, or thread its value
into the simplifier logic: check preserveBoundaries inside SimplifyMesh,
SimplifyMeshToTarget, SimplifyMeshByVoxelization and DeduplicateVertices and
modify edge/collapse selection to skip collapsing or altering edges that are
boundary edges (edges with only one adjacent face). Ensure the builder methods
remain consistent with the chosen approach (remove builder when removing the
field, or keep and honor the flag when implementing).

Comment on lines +272 to +275
/// Entity ID of the rigid body to attach to (optional)
/// When set, kinematic vertices will follow this body's transform
/// Use AttachToEntity() to set this properly
std::optional<Engine::EntityId> attachedEntityId = std::nullopt;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documentation references nonexistent AttachToEntity() method.

Line 274 says "Use AttachToEntity() to set this properly" but no such method exists on the SoftBody struct. Either add the method or update the comment to describe the correct usage pattern.

🤖 Prompt for AI Agents
In `@src/plugin/physics/src/component/SoftBody.hpp` around lines 272 - 275, The
comment on SoftBody::attachedEntityId references a nonexistent AttachToEntity()
method; update the comment or add the method. Either implement a
SoftBody::AttachToEntity(Engine::EntityId) method (or similarly named attach
function) that sets attachedEntityId and documents intended behavior, or change
the inline comment for attachedEntityId to refer to the actual API used to
attach (e.g., set attachedEntityId directly or call the existing attach function
name) so the doc matches the code.

Comment on lines +58 to +100
/**
* @brief Select kinematic vertex indices based on percentage (lowest Y values)
*
* This function selects a percentage of vertices to be kinematic (fixed to parent).
* It prioritizes vertices with the lowest Y coordinate (bottom of mesh).
*
* @param vertices The deduplicated vertices
* @param percent Percentage of vertices to select [0.0 - 1.0]
* @return Vector of vertex indices to make kinematic
*/
static std::vector<uint32_t> SelectKinematicVerticesByY(const std::vector<glm::vec3> &vertices, float percent)
{
if (vertices.empty() || percent <= 0.0f)
return {};

// Special case: 100% kinematic - return all vertices
if (percent >= 1.0f)
{
std::vector<uint32_t> allIndices;
allIndices.reserve(vertices.size());
for (uint32_t i = 0; i < vertices.size(); ++i)
{
allIndices.push_back(i);
}
return allIndices;
}

// For partial kinematic, use uniform spatial distribution instead of just Y
// This ensures anchor points are spread across the entire mesh
size_t count = static_cast<size_t>(std::ceil(vertices.size() * std::clamp(percent, 0.0f, 1.0f)));
count = std::max(count, size_t(1));

std::vector<uint32_t> kinematicIndices;
kinematicIndices.reserve(count);

size_t stride = std::max(size_t(1), vertices.size() / count);
for (size_t i = 0; kinematicIndices.size() < count && i < vertices.size(); i += stride)
{
kinematicIndices.push_back(static_cast<uint32_t>(i));
}

return kinematicIndices;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Function name and documentation are inconsistent with implementation.

The function is named SelectKinematicVerticesByY and the doc comment states it "prioritizes vertices with the lowest Y coordinate," but the actual implementation (lines 85–97) uses a uniform stride-based distribution with no Y-sorting at all. The comment on line 85 even acknowledges this: "use uniform spatial distribution instead of just Y."

Either rename the function (e.g., SelectKinematicVerticesUniform) and update the doc, or restore the Y-based selection logic to match the current contract.

🤖 Prompt for AI Agents
In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 58 - 100, The
implementation of SelectKinematicVerticesByY does not match its name/docs — it
selects vertices via a uniform stride instead of by lowest Y; either restore
Y-based selection or rename and update docs. To fix: if you want Y-based
behavior, sort or index the input vertices by glm::vec3.y (preserving original
indices), select the lowest N indices (compute count using std::ceil with
std::clamp like currently done) and return those indices; update variables like
kinematicIndices accordingly and keep the 0%/100% short-circuits. Alternatively,
if you prefer uniform selection, rename SelectKinematicVerticesByY to
SelectKinematicVerticesUniform (and update the function comment to describe
uniform/stride selection) and adjust any callers to use the new name.

Comment on lines +494 to +514
if (softBody.attachedEntityId.has_value())
{
// Get the attached body's BodyID
auto attachedEntity = softBody.attachedEntityId.value();
if (auto *rigidInternal = registry.try_get<Component::RigidBodyInternal>(attachedEntity))
{
attachedBodyID = rigidInternal->bodyID;
Log::Info(
fmt::format("SoftBody: Attaching to rigid body entity {}", static_cast<uint32_t>(attachedEntity)));
}
else if (auto *vehicleInternal = registry.try_get<Component::VehicleInternal>(attachedEntity))
{
attachedBodyID = vehicleInternal->chassisBodyID;
Log::Info(
fmt::format("SoftBody: Attaching to vehicle entity {}", static_cast<uint32_t>(attachedEntity)));
}
else
{
Log::Warn(
"SoftBody: attachedEntityId specified but entity has no RigidBodyInternal or VehicleInternal");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing entity validity check before accessing attached entity's components.

softBody.attachedEntityId.value() is a user-provided entity ID. If that entity has been destroyed or was never created, calling registry.try_get<...>(attachedEntity) on an invalid entity is undefined behavior in entt. Add a validity check before the lookups.

🛡️ Proposed fix
         if (softBody.attachedEntityId.has_value())
         {
             // Get the attached body's BodyID
             auto attachedEntity = softBody.attachedEntityId.value();
+            if (!registry.valid(attachedEntity))
+            {
+                Log::Warn("SoftBody: attachedEntityId refers to an invalid entity");
+            }
-            if (auto *rigidInternal = registry.try_get<Component::RigidBodyInternal>(attachedEntity))
+            else if (auto *rigidInternal = registry.try_get<Component::RigidBodyInternal>(attachedEntity))
🤖 Prompt for AI Agents
In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 494 - 514, The
code reads softBody.attachedEntityId.value() and calls
registry.try_get<...>(attachedEntity) without verifying the entity exists;
update the SoftBodySystem code to first check registry.valid(attachedEntity) (or
equivalent) after obtaining attachedEntity and before any try_get calls, and if
the entity is not valid log a warning (similar to the existing message) and skip
the attachment logic so attachedBodyID is not set from an invalid entity.

Comment on lines +709 to +727
{
if (auto *convexHullMesh = registry.try_get<Component::ConvexHullMeshCollider>(entity);
convexHullMesh && convexHullMesh->mesh && !convexHullMesh->mesh->GetVertices().empty())
{
mesh = &(convexHullMesh->mesh.value());
Log::Warn("SoftBody: Using ConvexHullMeshCollider mesh as geometry for SoftBody");
}
else if (auto *meshCollider = registry.try_get<Component::MeshCollider>(entity);
meshCollider && meshCollider->mesh && !meshCollider->mesh->GetVertices().empty())
{
mesh = &(meshCollider->mesh.value());
Log::Warn("SoftBody: Using MeshCollider mesh as geometry for SoftBody");
}
else
{
Log::Warn("SoftBody: No Mesh component found during sync. Skipping vertex update.");
continue;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Per-frame Log::Warn in the sync loop will spam logs every physics tick.

Lines 714 and 720 emit a warning every time SyncSoftBodyVertices runs (every fixed-timestep tick) if the entity uses a collider mesh instead of a dedicated Mesh component. This will produce massive log output during normal operation.

Either suppress these after the first occurrence (e.g., a flag on the internal component) or downgrade to Log::Debug.

🤖 Prompt for AI Agents
In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 709 - 727, The
per-tick Log::Warn calls inside the SyncSoftBodyVertices loop (the block that
checks ConvexHullMeshCollider and MeshCollider and currently calls Log::Warn
"SoftBody: Using ... mesh as geometry for SoftBody" or "SoftBody: No Mesh
component found...") will spam logs; either change these Log::Warn calls to
Log::Debug, or add a one-time flag (e.g., a bool on the SoftBody component or an
internal map keyed by entity) that records that the warning has been emitted and
only logs on the first occurrence, setting the flag after emitting; update the
calls referring to ConvexHullMeshCollider, MeshCollider and the "No Mesh" path
accordingly and ensure the continue behavior remains unchanged.

if (!physicsManager.IsPhysicsActivated())
return;

auto &bodyInterface = physicsManager.GetBodyInterface();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unused variable bodyInterface.

bodyInterface is fetched on line 842 but never referenced in UpdateKinematicVertices. Remove it to avoid the unused-variable warning.

🧹 Proposed fix
     if (!physicsManager.IsPhysicsActivated())
         return;
 
-    auto &bodyInterface = physicsManager.GetBodyInterface();
-
     // Update kinematic vertex positions for attached soft bodies
📝 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
auto &bodyInterface = physicsManager.GetBodyInterface();
🤖 Prompt for AI Agents
In `@src/plugin/physics/src/system/SoftBodySystem.cpp` at line 842, Remove the
unused local variable by deleting the line that assigns auto &bodyInterface =
physicsManager.GetBodyInterface(); inside UpdateKinematicVertices; if any later
code intended to use the BodyInterface, instead access it where needed via
physicsManager.GetBodyInterface() inline or introduce a used variable with a
clear name, but otherwise remove the unused declaration to eliminate the
warning.

@MasterLaplace MasterLaplace force-pushed the softbody-attachement-and-mesh-simplifier branch from f757131 to 1de556d Compare February 15, 2026 11:12
@MasterLaplace MasterLaplace changed the title Softbody attachement and mesh simplifier feat: add Softbody attachement and mesh simplifier Feb 15, 2026
@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
3.5% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

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.

1 participant