Skip to content

feat: Add compound shape and convex decomposition support#361

Open
hsheth2 wants to merge 7 commits intodimforge:masterfrom
hsheth2:compound-collider
Open

feat: Add compound shape and convex decomposition support#361
hsheth2 wants to merge 7 commits intodimforge:masterfrom
hsheth2:compound-collider

Conversation

@hsheth2
Copy link

@hsheth2 hsheth2 commented Dec 27, 2025

Closes #44 and closes #226 and supersedes #303.

  • Added support for compound shapes and convex decomposition to the JavaScript bindings
  • The API uses separate arrays for shapes/positions/rotations (matches existing joint API pattern, unlike wip untested compound shapes #303 which created an Isometry class)
  • Added two testbed3d demos showcasing the features, and tested these manually in my browser.

Known limitation: compound shapes cannot be fully deserialized from snapshots (the Rust/WASM layer doesn't expose sub-shape query APIs). This only affects save/load workflows that need to inspect compound internals - normal creation and physics simulation work fine.

Disclaimer: I used Claude Code to produce this PR, but have manually reviewed the code myself already.

@hsheth2
Copy link
Author

hsheth2 commented Jan 30, 2026

Hey @sebcrozet, let me know if there's any blockers for getting this merged in

Copy link
Member

@sebcrozet sebcrozet left a comment

Choose a reason for hiding this comment

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

Hey! Took me some time to get to it, but thank you for this PR.

return null;
}

// Create a wrapper - note: we can't deserialize compound shapes yet,
Copy link
Member

Choose a reason for hiding this comment

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

The comment is misleading here. The issue isn’t related to deserialization, it’s related to the fact that we don’t have any accessors in rawShape to retrieve the list of its primitives. I think we should:

  1. Add the relevant accessors on the rust side so we can get the primitive count, list, and transforms, from a compound shape.
  2. Add explicitely a class RawCompound extends Shape instead of creating an anonymous class here.
  3. Add a method to RawCompound for converting it to a Compound (by calling the accessors from (1)).
  4. This should also allow us to provide an actual implementation of deserialization of a Compound shape instead of throwing an exception.

Comment on lines +58 to +60
const shape1 = new RAPIER.Cuboid(0.8, 0.4, 0.5);
const shape2 = new RAPIER.Cuboid(0.8, 0.4, 0.5);
const shape3 = new RAPIER.Cuboid(0.8, 0.4, 0.5);
Copy link
Member

Choose a reason for hiding this comment

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

Instead of redefining te same shape three times, it can be defined just once and appear three times in the array shapes = [shape, shape, shape].

Comment on lines +541 to +576
#[cfg(feature = "dim3")]
pub fn convexDecompositionWithParams(
vertices: Vec<f32>,
indices: Vec<u32>,
params: &RawVHACDParameters,
) -> Option<RawShape> {
let vertices: Vec<_> = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect();
let indices: Vec<_> = indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect();

let shape = SharedShape::convex_decomposition_with_params(&vertices, &indices, &params.0);
Some(Self(shape))
}

#[cfg(feature = "dim2")]
pub fn convexDecomposition(vertices: Vec<f32>, indices: Vec<u32>) -> Option<RawShape> {
let vertices: Vec<_> = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect();
let indices: Vec<_> = indices.chunks(2).map(|v| [v[0], v[1]]).collect();

let shape =
SharedShape::convex_decomposition_with_params(&vertices, &indices, &Default::default());
Some(Self(shape))
}

#[cfg(feature = "dim2")]
pub fn convexDecompositionWithParams(
vertices: Vec<f32>,
indices: Vec<u32>,
params: &RawVHACDParameters,
) -> Option<RawShape> {
let vertices: Vec<_> = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect();
let indices: Vec<_> = indices.chunks(2).map(|v| [v[0], v[1]]).collect();

let shape = SharedShape::convex_decomposition_with_params(&vertices, &indices, &params.0);
Some(Self(shape))
}

Copy link
Member

Choose a reason for hiding this comment

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

The only difference between the 2D and 3D versions here is the chunking of indices.
So instead of duplicating the functions for 2D and 3D, there should only be one version of each but with something like that inside:

        #[cfg(feature = "dim2")]
        let indices: Vec<_> = indices.chunks(2).map(|v| [v[0], v[1]]).collect();
        #[cfg(feature = "dim3")]
        let indices: Vec<_> = indices.chunks(3).map(|v| [v[0], v[1], v[2]]).collect();

Comment on lines +495 to +498
#[cfg(feature = "dim2")]
let pos_offset = i * 2;
#[cfg(feature = "dim3")]
let pos_offset = i * 3;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
#[cfg(feature = "dim2")]
let pos_offset = i * 2;
#[cfg(feature = "dim3")]
let pos_offset = i * 3;
let pos_offset = i * DIM;

use rapier::math::{Isometry, Point, Real, Vector, DIM};
use rapier::parry::query;
use rapier::parry::query::{Ray, ShapeCastOptions};
#[cfg(any(feature = "dim2", feature = "dim3"))]
Copy link
Member

Choose a reason for hiding this comment

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

This isn’t needed, we are always supposed to compile with either dim2 or dim3 enabled.

Suggested change
#[cfg(any(feature = "dim2", feature = "dim3"))]

@hsheth2
Copy link
Author

hsheth2 commented Mar 17, 2026

Thanks for the review @sebcrozet . I've largely addressed the comments, with exceptions below.

I ended up not adding a RawCompound type since it wasn't actually adding much versus the actual Compound, and added the from / into methods on the Compound directly.

I did run into some issues with the round-tripping of convex decomposition compounds - the comment on ConvexPolyhedron's intoRaw explains my workaround for it that preserves the RawShape + shape data so that the round-trip encode/decode works fine, but it does feel a bit hacky. The core problem is that for some deserialized convex-decomposition children (e.g. the ones coming out of VHACD), reconstructing a fresh raw child with RawShape.convexMesh(this.vertices, this.indices) returns undefined, which later causes RawShape.compound(...) to throw. The other alternative I considered was to just ignore this.indices when doing intoRaw for ConvexPolyhedron and instead recompute a convex hull from the vertices, which I think maintains correctness and would be simpler to implement but adds an extra step.

@hsheth2
Copy link
Author

hsheth2 commented Mar 17, 2026

Managed to work around that issue where compound shapes weren't round tripping properly - see the latest commit for how. That removes some of the hacks I needed to put in earlier.

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.

Convex Decomposition Support for compound shapes

2 participants