Skip to content

feat(constitution): EmbeddedCollisionMesh — barycentric coupling of passive surface mesh onto FEM tet mesh#461

Open
tcordeboeuf wants to merge 1 commit into
spiriMirror:mainfrom
tcordeboeuf:feat/embedded-collision-mesh
Open

feat(constitution): EmbeddedCollisionMesh — barycentric coupling of passive surface mesh onto FEM tet mesh#461
tcordeboeuf wants to merge 1 commit into
spiriMirror:mainfrom
tcordeboeuf:feat/embedded-collision-mesh

Conversation

@tcordeboeuf

Copy link
Copy Markdown

Motivation

When simulating characters or soft bodies, the FEM tet mesh is typically coarse for performance, but collision detection requires a dense, smooth surface. This contribution adds EmbeddedCollisionMesh, a constitution that drives a dense passive surface mesh from a coarse FEM tet simulation via barycentric interpolation — the same approach used in SOFA's BarycentricMapping.

What this adds

CPU side — constitution/

  • EmbeddedCollisionMesh::apply_to(tet_sc, surface_sc) — point-in-tet query (brute-force, fine at init), writes three attributes on the surface mesh:
    • ecm_tet_index (IndexT per vertex) — containing tet index
    • ecm_bary (Vector4 per vertex) — barycentric coordinates
    • ecm_driven (IndexT meta, value=1) — marks the mesh as driven

CUDA backend — finite_element/constraints/

Three systems registered as SimSystem:

Class Role
EmbeddedCollisionMeshVertexReporter Registers surface verts in GlobalVertexManager (after ABD/FEM/HalfPlane)
EmbeddedCollisionMeshSystem Holds GPU buffers; drives forward pass (x_surface = J * x_tet) and backward pass (grad_tet += Jᵀ * grad_surface)
EmbeddedCollisionMeshForceReporter Injects tet_extra_grad into the FEM linear system via FEMLinearSubsystemReporter

Engine hooks — two new SimSystem events

// subscribe in any SimSystem::do_build():
on_before_collision_detection([&]{ ... });  // forward pass
on_after_contact_assembly([&]{ ... });      // backward pass

These are general-purpose hooks placed around collision detection in advance_ipc.cu, useful for any future driven/passive body system.

Usage

EmbeddedCollisionMesh ecm;
ecm.apply_to(tet_sc, surface_sc);  // CPU — called once at scene build
// surface_sc is passive (no FEM constitution), tet_sc has a FEM constitution

Reference

  • SOFA BarycentricMappingapply() / applyJT()
  • Finite element embedding as used in Ziva VFX / SOFA for collision proxy meshes

…of passive surface mesh onto FEM tet mesh

Adds a new constitution that drives a dense passive surface mesh from
a coarse FEM tet simulation via barycentric interpolation (forward pass)
and scatters contact gradient back to tet DOFs (backward pass).

CPU side (constitution/):
- EmbeddedCollisionMesh::apply_to() — point-in-tet query, writes
  ecm_tet_index / ecm_bary / ecm_driven attributes on the surface mesh

CUDA backend (finite_element/constraints/):
- EmbeddedCollisionMeshVertexReporter — registers surface verts in GlobalVertexManager
- EmbeddedCollisionMeshSystem — forward kernel (x_surface = J * x_tet)
  and backward kernel (grad_tet += Jt * grad_surface) via atomicAdd
- EmbeddedCollisionMeshForceReporter — injects tet_extra_grad into FEM linear system

Engine hooks (two new SimSystem events):
- on_before_collision_detection / event_before_collision_detection — forward pass
- on_after_contact_assembly / event_after_contact_assembly — backward pass

Reference: SOFA BarycentricMapping (apply / applyJT)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tcordeboeuf tcordeboeuf force-pushed the feat/embedded-collision-mesh branch from edde6c6 to f13acd9 Compare May 22, 2026 12:41

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces the EmbeddedCollisionMesh system, which implements barycentric coupling to drive a dense surface mesh using a coarse FEM tetrahedral mesh. The implementation includes CPU-side logic for calculating initial embedding coordinates and a CUDA backend for forward position updates and backward force scattering. Feedback highlights critical issues, including a missing attribute (ecm_tet_geo_id) required for backend initialization and a mismatch between local and global tetrahedron indices. Other concerns involve the current limitation to a single embedded mesh per scene, the use of const_cast to bypass standard vertex reporting flows, and potential performance bottlenecks due to the brute-force point-in-tet search. A code suggestion was also provided to add a static_assert ensuring proper memory alignment for atomic operations.

Comment on lines +222 to +223
auto tet_id_attr = sc->meta().find<IndexT>("ecm_tet_geo_id");
if(!tet_id_attr)

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.

critical

The backend searches for the ecm_tet_geo_id attribute to identify the driving tet mesh, but this attribute is never created or populated in the EmbeddedCollisionMesh::apply_to implementation in embedded_collision_mesh.cpp. This will cause the initialization to fail and the system to remain inactive because tet_sc will always be null.


if(is_inside(bary))
{
tet_index_view[si] = (IndexT)ti;

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.

critical

The tet_index_view stores the local index of the tetrahedron within tet_sc. However, the GPU backend in embedded_collision_mesh.cu uses this index to access fem.tets(), which is a global buffer containing all tetrahedra in the simulation. If tet_sc is not the first tet mesh in the scene, these indices will point to the wrong tetrahedra. The backend needs to account for the global tetrahedron offset of the driving mesh.

tet_sc = geo_slots[tet_gi]
->geometry()
.as<geometry::SimplicialComplex>();
break;

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.

high

The initialization logic and the EmbeddedCollisionMeshData structure are currently limited to a single embedded mesh per scene. The loop breaks after finding the first geometry with the ecm_driven attribute, and the GPU buffers are not managed as a collection. This prevents the simulation of multiple independent objects using embedded collision meshes.


// Write directly into the GlobalVertexManager positions buffer at the
// surface mesh offset — this is what the CCD and contact systems see.
Vector3* surf_ptr = const_cast<Vector3*>(all_pos.data()) + d.surface_vertex_offset;

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.

medium

Using const_cast to write directly into the GlobalVertexManager positions buffer bypasses the intended reporter-based update flow. While this avoids a copy, it makes the system highly dependent on the internal implementation of the vertex manager and could lead to synchronization issues if the manager expects to control all writes to its buffers.

coo_count,
(IndexT)d.surface_vertex_offset,
(int)d.n_surface_verts,
reinterpret_cast<Float*>(d.surface_grad_dense.data()));

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.

medium

Reinterpreting Vector3* as Float* for component-wise atomicAdd assumes that the Vector3 type (typically an Eigen type) has no internal padding or alignment overhead. A static_assert should be added to ensure this holds true across different platforms and Eigen configurations.

static_assert(sizeof(Vector3) == 3 * sizeof(Float), "Vector3 must not have padding for component-wise atomicAdd");
                reinterpret_cast<Float*>(d.surface_grad_dense.data()));

Comment on lines +91 to +92
// Brute-force O(n_surface * n_tets) — fine at init.
// Replace with BVH if tet count > ~10k.

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.

medium

The $O(N_{surface} \cdot N_{tets})$ brute-force search for point-in-tet queries will be a significant bottleneck for dense surface meshes or high-resolution tet meshes. For a typical character setup (e.g., 100k surface vertices and 10k tets), this results in $10^9$ iterations, which can cause multi-second hangs during scene building. A spatial acceleration structure like a BVH or a simple spatial hash should be implemented to improve performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant