From 8e589e49c4da767da9d2d80170af89ea984fadc5 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Fri, 26 Dec 2025 20:54:49 -0500 Subject: [PATCH 1/5] update cargo.lock w/ new version --- Cargo.lock | 73 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d2a5111..97d941f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,12 +338,13 @@ dependencies = [ [[package]] name = "dimforge_rapier2d" -version = "0.19.2" +version = "0.19.3" dependencies = [ "bincode", "js-sys", "nalgebra", "palette", + "parry2d", "rapier2d", "ref-cast", "serde", @@ -352,12 +353,13 @@ dependencies = [ [[package]] name = "dimforge_rapier2d-deterministic" -version = "0.19.2" +version = "0.19.3" dependencies = [ "bincode", "js-sys", "nalgebra", "palette", + "parry2d", "rapier2d", "ref-cast", "serde", @@ -366,12 +368,13 @@ dependencies = [ [[package]] name = "dimforge_rapier2d-simd" -version = "0.19.2" +version = "0.19.3" dependencies = [ "bincode", "js-sys", "nalgebra", "palette", + "parry2d", "rapier2d", "ref-cast", "serde", @@ -380,12 +383,13 @@ dependencies = [ [[package]] name = "dimforge_rapier3d" -version = "0.19.2" +version = "0.19.3" dependencies = [ "bincode", "js-sys", "nalgebra", "palette", + "parry3d", "rapier3d", "ref-cast", "serde", @@ -394,12 +398,13 @@ dependencies = [ [[package]] name = "dimforge_rapier3d-deterministic" -version = "0.19.2" +version = "0.19.3" dependencies = [ "bincode", "js-sys", "nalgebra", "palette", + "parry3d", "rapier3d", "ref-cast", "serde", @@ -408,12 +413,13 @@ dependencies = [ [[package]] name = "dimforge_rapier3d-simd" -version = "0.19.2" +version = "0.19.3" dependencies = [ "bincode", "js-sys", "nalgebra", "palette", + "parry3d", "rapier3d", "ref-cast", "serde", @@ -578,9 +584,9 @@ checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" [[package]] name = "glam" -version = "0.30.8" +version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12d847aeb25f41be4c0ec9587d624e9cd631bc007a8fd7ce3f5851e064c6460" +checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" [[package]] name = "globset" @@ -615,6 +621,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" @@ -628,12 +640,13 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] @@ -703,12 +716,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.14.5", "serde", ] @@ -796,7 +809,7 @@ dependencies = [ "glam 0.27.0", "glam 0.28.0", "glam 0.29.3", - "glam 0.30.8", + "glam 0.30.9", "matrixmultiply", "nalgebra-macros", "num-complex", @@ -926,9 +939,9 @@ dependencies = [ [[package]] name = "parry2d" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ea16e5cdf52dd91b2a98ef781a2121f48dfb0880a92ae1ec6bc9e78097fceb" +checksum = "ef681740349cec3ab9b5996b03b459b383b6998e1ffcb2804e8b57eb1e8491d9" dependencies = [ "approx", "arrayvec", @@ -937,7 +950,7 @@ dependencies = [ "either", "ena", "foldhash 0.2.0", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "indexmap", "log", "nalgebra", @@ -955,9 +968,9 @@ dependencies = [ [[package]] name = "parry3d" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2b4291e3c8fcba5f514ed627228c92fa4c94c38bb202c35be7b3b021090193" +checksum = "e99471b7b6870f7fe406d5611dd4b4c9b07aa3e5436b1d27e1515f9832bb0c6b" dependencies = [ "approx", "arrayvec", @@ -966,8 +979,8 @@ dependencies = [ "either", "ena", "foldhash 0.2.0", - "glam 0.30.8", - "hashbrown 0.16.0", + "glam 0.30.9", + "hashbrown 0.16.1", "indexmap", "log", "nalgebra", @@ -1353,10 +1366,11 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -1369,11 +1383,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", From a212a0c870f0bb04c9d38992122a359a44965eb4 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Fri, 26 Dec 2025 20:54:59 -0500 Subject: [PATCH 2/5] add compound collider + convexDecomposition routine --- src.ts/geometry/collider.ts | 48 +++++++++++++++- src.ts/geometry/shape.ts | 106 +++++++++++++++++++++++++++++++++++- src/geometry/shape.rs | 58 ++++++++++++++++++++ 3 files changed, 209 insertions(+), 3 deletions(-) diff --git a/src.ts/geometry/collider.ts b/src.ts/geometry/collider.ts index 25817362..20e66726 100644 --- a/src.ts/geometry/collider.ts +++ b/src.ts/geometry/collider.ts @@ -1,4 +1,4 @@ -import {RawColliderSet} from "../raw"; +import {RawColliderSet, RawShape} from "../raw"; import {Rotation, RotationOps, Vector, VectorOps} from "../math"; import { CoefficientCombineRule, @@ -18,6 +18,7 @@ import { TriMesh, Polyline, Heightfield, + Compound, Segment, Triangle, RoundTriangle, @@ -1690,6 +1691,51 @@ export class ColliderDesc { // #endif + /** + * Creates a new collider descriptor with a compound shape. + * + * @param shapes - The array of shapes composing this compound. + * @param positions - The array of positions for each shape (relative to the compound's origin). + * @param rotations - The array of rotations for each shape (relative to the compound's orientation). + */ + public static compound( + shapes: Shape[], + positions: Vector[], + rotations: Rotation[], + ): ColliderDesc { + const shape = new Compound(shapes, positions, rotations); + return new ColliderDesc(shape); + } + + /** + * Creates a new collider descriptor with a compound shape automatically created + * from a convex decomposition of the given triangle mesh. + * + * @param vertices - The coordinates of the mesh's vertices. + * @param indices - The indices of the mesh's triangles. + */ + public static convexDecomposition( + vertices: Float32Array, + indices: Uint32Array, + ): ColliderDesc | null { + const rawShape = RawShape.convexDecomposition(vertices, indices); + + if (!rawShape) { + return null; + } + + // Create a wrapper - note: we can't deserialize compound shapes yet, + // so we create a minimal wrapper that just holds the raw shape + const shape = new (class extends Shape { + readonly type = ShapeType.Compound; + intoRaw(): RawShape { + return rawShape; + } + })(); + + return new ColliderDesc(shape); + } + // #if DIM2 /** * Sets the position of the collider to be created relative to the rigid-body it is attached to. diff --git a/src.ts/geometry/shape.ts b/src.ts/geometry/shape.ts index f3546982..05432363 100644 --- a/src.ts/geometry/shape.ts +++ b/src.ts/geometry/shape.ts @@ -197,6 +197,17 @@ export abstract class Shape { return new RoundCone(halfHeight, radius, borderRadius); // #endif + case RawShapeType.Compound: + // For compound shapes, we can't deserialize the sub-shapes from WASM, + // so we create a minimal placeholder that can't be reconstructed + return new (class extends Shape { + readonly type = ShapeType.Compound; + intoRaw(): RawShape { + throw new Error( + "Cannot reconstruct compound shapes from deserialized colliders", + ); + } + })(); default: throw new Error("unknown shape type: " + rawType); } @@ -515,7 +526,7 @@ export enum ShapeType { Triangle = 5, TriMesh = 6, HeightField = 7, - // Compound = 8, + Compound = 8, ConvexPolygon = 9, RoundCuboid = 10, RoundTriangle = 11, @@ -540,7 +551,7 @@ export enum ShapeType { Triangle = 5, TriMesh = 6, HeightField = 7, - // Compound = 8, + Compound = 8, ConvexPolyhedron = 9, Cylinder = 10, Cone = 11, @@ -1065,6 +1076,97 @@ export class Voxels extends Shape { } } +/** + * A compound shape, consisting of multiple sub-shapes with relative positions. + */ +export class Compound extends Shape { + readonly type = ShapeType.Compound; + + /** + * The shapes composing this compound shape. + */ + shapes: Shape[]; + + /** + * The positions of each sub-shape relative to the compound's origin. + */ + positions: Vector[]; + + /** + * The rotations of each sub-shape relative to the compound's orientation. + */ + rotations: Rotation[]; + + /** + * Creates a new compound shape. + * + * @param shapes - The array of shapes composing this compound. + * @param positions - The array of positions for each shape. + * @param rotations - The array of rotations for each shape. + */ + constructor(shapes: Shape[], positions: Vector[], rotations: Rotation[]) { + super(); + + if ( + shapes.length !== positions.length || + shapes.length !== rotations.length + ) { + throw new Error( + "shapes, positions, and rotations arrays must have the same length", + ); + } + + this.shapes = shapes; + this.positions = positions; + this.rotations = rotations; + } + + public intoRaw(): RawShape { + const rawShapes = this.shapes.map((s) => s.intoRaw()); + + // #if DIM2 + // Flatten positions into a single array + const positions = new Float32Array(this.positions.length * 2); + this.positions.forEach((pos, i) => { + positions[i * 2] = pos.x; + positions[i * 2 + 1] = pos.y; + }); + + // Flatten rotations (single angle for 2D) + const rotations = new Float32Array(this.rotations.length); + this.rotations.forEach((rot, i) => { + rotations[i] = rot; + }); + // #endif + + // #if DIM3 + // Flatten positions into a single array + const positions = new Float32Array(this.positions.length * 3); + this.positions.forEach((pos, i) => { + positions[i * 3] = pos.x; + positions[i * 3 + 1] = pos.y; + positions[i * 3 + 2] = pos.z; + }); + + // Flatten rotations (quaternion for 3D: x, y, z, w) + const rotations = new Float32Array(this.rotations.length * 4); + this.rotations.forEach((rot, i) => { + rotations[i * 4] = rot.x; + rotations[i * 4 + 1] = rot.y; + rotations[i * 4 + 2] = rot.z; + rotations[i * 4 + 3] = rot.w; + }); + // #endif + + const result = RawShape.compound(rawShapes, positions, rotations); + + // Free temporary raw shapes (they're cloned by compound()) + rawShapes.forEach((s) => s.free()); + + return result; + } +} + /** * A shape that is a triangle mesh. */ diff --git a/src/geometry/shape.rs b/src/geometry/shape.rs index 5f01d9f5..bb5909d3 100644 --- a/src/geometry/shape.rs +++ b/src/geometry/shape.rs @@ -401,6 +401,64 @@ impl RawShape { SharedShape::round_convex_mesh(vertices, &indices, borderRadius).map(|s| Self(s)) } + pub fn compound(shapes: Vec, positions: Vec, rotations: Vec) -> RawShape { + let mut compound_parts = Vec::new(); + let num_shapes = shapes.len(); + + for i in 0..num_shapes { + #[cfg(feature = "dim2")] + let pos_offset = i * 2; + #[cfg(feature = "dim3")] + let pos_offset = i * 3; + + #[cfg(feature = "dim2")] + let translation = Vector::new(positions[pos_offset], positions[pos_offset + 1]).into(); + + #[cfg(feature = "dim3")] + let translation = Vector::new( + positions[pos_offset], + positions[pos_offset + 1], + positions[pos_offset + 2], + ).into(); + + #[cfg(feature = "dim2")] + let rotation = na::UnitComplex::new(rotations[i]); + + #[cfg(feature = "dim3")] + let rotation = { + let rot_offset = i * 4; + na::UnitQuaternion::from_quaternion(na::Quaternion::new( + rotations[rot_offset + 3], // w + rotations[rot_offset], // x + rotations[rot_offset + 1], // y + rotations[rot_offset + 2], // z + )) + }; + + let isometry = Isometry::from_parts(translation, rotation); + compound_parts.push((isometry, shapes[i].0.clone())); + } + + Self(SharedShape::compound(compound_parts)) + } + + #[cfg(feature = "dim3")] + pub fn convexDecomposition(vertices: Vec, indices: Vec) -> Option { + 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(); + + SharedShape::convex_decomposition(&vertices, &indices).map(Self) + } + + #[cfg(feature = "dim2")] + pub fn convexDecomposition(vertices: Vec, indices: Vec) -> Option { + 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)) + } + pub fn castShape( &self, shapePos1: &RawVector, From 07b03f26153ee2b2ae771feddf0de4cb2c48f91b Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Sat, 27 Dec 2025 00:37:13 -0500 Subject: [PATCH 3/5] fix bugs + add 3d testbed examples --- src.ts/geometry/shape.ts | 11 +- src/geometry/shape.rs | 3 +- testbed3d/src/demos/compoundShapes.ts | 192 +++++++++++++++++++++ testbed3d/src/demos/convexDecomposition.ts | 156 +++++++++++++++++ testbed3d/src/index.ts | 4 + 5 files changed, 355 insertions(+), 11 deletions(-) create mode 100644 testbed3d/src/demos/compoundShapes.ts create mode 100644 testbed3d/src/demos/convexDecomposition.ts diff --git a/src.ts/geometry/shape.ts b/src.ts/geometry/shape.ts index 05432363..9511b2a7 100644 --- a/src.ts/geometry/shape.ts +++ b/src.ts/geometry/shape.ts @@ -1125,14 +1125,12 @@ export class Compound extends Shape { const rawShapes = this.shapes.map((s) => s.intoRaw()); // #if DIM2 - // Flatten positions into a single array const positions = new Float32Array(this.positions.length * 2); this.positions.forEach((pos, i) => { positions[i * 2] = pos.x; positions[i * 2 + 1] = pos.y; }); - // Flatten rotations (single angle for 2D) const rotations = new Float32Array(this.rotations.length); this.rotations.forEach((rot, i) => { rotations[i] = rot; @@ -1140,7 +1138,6 @@ export class Compound extends Shape { // #endif // #if DIM3 - // Flatten positions into a single array const positions = new Float32Array(this.positions.length * 3); this.positions.forEach((pos, i) => { positions[i * 3] = pos.x; @@ -1148,7 +1145,6 @@ export class Compound extends Shape { positions[i * 3 + 2] = pos.z; }); - // Flatten rotations (quaternion for 3D: x, y, z, w) const rotations = new Float32Array(this.rotations.length * 4); this.rotations.forEach((rot, i) => { rotations[i * 4] = rot.x; @@ -1158,12 +1154,7 @@ export class Compound extends Shape { }); // #endif - const result = RawShape.compound(rawShapes, positions, rotations); - - // Free temporary raw shapes (they're cloned by compound()) - rawShapes.forEach((s) => s.free()); - - return result; + return RawShape.compound(rawShapes, positions, rotations); } } diff --git a/src/geometry/shape.rs b/src/geometry/shape.rs index bb5909d3..27f97cec 100644 --- a/src/geometry/shape.rs +++ b/src/geometry/shape.rs @@ -447,7 +447,8 @@ impl 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(); - SharedShape::convex_decomposition(&vertices, &indices).map(Self) + let shape = SharedShape::convex_decomposition(&vertices, &indices); + Some(Self(shape)) } #[cfg(feature = "dim2")] diff --git a/testbed3d/src/demos/compoundShapes.ts b/testbed3d/src/demos/compoundShapes.ts new file mode 100644 index 00000000..1e821c1b --- /dev/null +++ b/testbed3d/src/demos/compoundShapes.ts @@ -0,0 +1,192 @@ +import type {Testbed} from "../Testbed"; +import seedrandom from "seedrandom"; + +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +function createLShape(RAPIER: RAPIER_API): any { + const shape1 = new RAPIER.Cuboid(2.0, 0.5, 0.5); + const shape2 = new RAPIER.Cuboid(0.5, 1.5, 0.5); + + const shapes = [shape1, shape2]; + const positions = [ + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Vector3(-1.5, -1.5, 0.0), + ]; + const rotations = [ + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ]; + + return RAPIER.ColliderDesc.compound(shapes, positions, rotations); +} + +function createTShape(RAPIER: RAPIER_API): any { + const shape1 = new RAPIER.Cuboid(2.0, 0.5, 0.5); + const shape2 = new RAPIER.Cuboid(0.5, 1.5, 0.5); + + const shapes = [shape1, shape2]; + const positions = [ + new RAPIER.Vector3(0.0, 1.0, 0.0), + new RAPIER.Vector3(0.0, -1.0, 0.0), + ]; + const rotations = [ + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ]; + + return RAPIER.ColliderDesc.compound(shapes, positions, rotations); +} + +function createPlusShape(RAPIER: RAPIER_API): any { + const shape1 = new RAPIER.Cuboid(2.0, 0.5, 0.5); + const shape2 = new RAPIER.Cuboid(0.5, 2.0, 0.5); + + const shapes = [shape1, shape2]; + const positions = [ + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Vector3(0.0, 0.0, 0.0), + ]; + const rotations = [ + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ]; + + return RAPIER.ColliderDesc.compound(shapes, positions, rotations); +} + +function createStairsShape(RAPIER: RAPIER_API): any { + 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); + + const shapes = [shape1, shape2, shape3]; + // Bottom, middle, top steps respectively. + const positions = [ + new RAPIER.Vector3(-1.2, -0.8, 0.0), + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Vector3(1.2, 0.8, 0.0), + ]; + const rotations = [ + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ]; + + return RAPIER.ColliderDesc.compound(shapes, positions, rotations); +} + +function createDumbbellShape(RAPIER: RAPIER_API): any { + const shape1 = new RAPIER.Ball(0.8); // left weight + const shape2 = new RAPIER.Cuboid(1.5, 0.2, 0.2); // bar + const shape3 = new RAPIER.Ball(0.8); // right weight + + const shapes = [shape1, shape2, shape3]; + const positions = [ + new RAPIER.Vector3(-2.0, 0.0, 0.0), + new RAPIER.Vector3(0.0, 0.0, 0.0), + new RAPIER.Vector3(2.0, 0.0, 0.0), + ]; + const rotations = [ + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + new RAPIER.Quaternion(0.0, 0.0, 0.0, 1.0), + ]; + + return RAPIER.ColliderDesc.compound(shapes, positions, rotations); +} + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + testbed.parameters.debugRender = true; + + // Create Ground - a large platform + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let groundBody = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(15.0, 0.5, 15.0); + world.createCollider(colliderDesc, groundBody); + + for (let i = 0; i < 3; i++) { + const x = (i - 1) * 8; + const bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation(x, 1.0, 0.0); + const body = world.createRigidBody(bodyDesc); + const colliderDesc = RAPIER.ColliderDesc.cylinder(2.0, 0.5); + world.createCollider(colliderDesc, body); + } + + const rng = seedrandom("compoundShapes"); + + const shapeCreators = [ + createLShape, + createTShape, + createPlusShape, + createStairsShape, + createDumbbellShape, + ]; + + const gridSize = 4; + const spacing = 6.0; + const startHeight = 15.0; + + for (let i = 0; i < gridSize; i++) { + for (let j = 0; j < gridSize; j++) { + const x = (i - gridSize / 2) * spacing; + const z = (j - gridSize / 2) * spacing; + const y = startHeight + i * 3.0; + + const shapeCreator = + shapeCreators[Math.floor(rng() * shapeCreators.length)]; + + const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + x, + y, + z, + ); + const body = world.createRigidBody(bodyDesc); + const colliderDesc = shapeCreator(RAPIER); + world.createCollider(colliderDesc, body); + body.setAngvel( + new RAPIER.Vector3( + (rng() - 0.5) * 3, + (rng() - 0.5) * 3, + (rng() - 0.5) * 3, + ), + true, + ); + body.setLinvel( + new RAPIER.Vector3( + (rng() - 0.5) * 2, + -1.0, + (rng() - 0.5) * 2, + ), + true, + ); + } + } + + for (let i = 0; i < 3; i++) { + const side = i % 2 === 0 ? -12 : 12; + const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation( + side, + 20.0 + i * 4, + 0.0, + ); + const body = world.createRigidBody(bodyDesc); + const colliderDesc = createDumbbellShape(RAPIER); + world.createCollider(colliderDesc, body); + body.setLinvel(new RAPIER.Vector3(side * -1.5, 0.0, 0.0), true); + body.setAngvel( + new RAPIER.Vector3(0.0, (rng() - 0.5) * 4, 0.0), + true, + ); + } + + testbed.setWorld(world); + + let cameraPosition = { + eye: {x: 30.0, y: 25.0, z: 30.0}, + target: {x: 0.0, y: 10.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/testbed3d/src/demos/convexDecomposition.ts b/testbed3d/src/demos/convexDecomposition.ts new file mode 100644 index 00000000..a9bc79bf --- /dev/null +++ b/testbed3d/src/demos/convexDecomposition.ts @@ -0,0 +1,156 @@ +import type {Testbed} from "../Testbed"; +import { + Vector3, + Object3D, + Mesh, + BufferGeometry, + BufferAttribute, + MeshPhongMaterial, + Color, +} from "three"; +import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader"; +type RAPIER_API = typeof import("@dimforge/rapier3d"); + +export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { + let gravity = new RAPIER.Vector3(0.0, -9.81, 0.0); + let world = new RAPIER.World(gravity); + + testbed.parameters.debugRender = true; + + // Create Ground. + let bodyDesc = RAPIER.RigidBodyDesc.fixed(); + let groundBody = world.createRigidBody(bodyDesc); + let colliderDesc = RAPIER.ColliderDesc.cuboid(10.0, 0.1, 10.0); + world.createCollider(colliderDesc, groundBody); + + let loader = new GLTFLoader(); + + const positions = [ + {x: -4, y: 15, z: 0}, + {x: 0, y: 18, z: 0}, + {x: 4, y: 21, z: 0}, + ]; + + const colors = [ + [0xff6b6b, 0x4ecdc4, 0x45b7d1, 0xf9ca24, 0xee5a6f], + [0x6c5ce7, 0xa29bfe, 0xfd79a8, 0xfdcb6e, 0xe17055], + [0x00b894, 0x00cec9, 0x0984e3, 0x6c5ce7, 0xfd79a8], + ]; + + positions.forEach((pos, index) => { + loader.load("./suzanne_blender_monkey.glb", (gltf) => { + const scale = 2.0; + gltf.scene.position.set(pos.x, pos.y, pos.z); + gltf.scene.scale.set(scale, scale, scale); + gltf.scene.updateMatrixWorld(true); + + const v = new Vector3(); + const meshVertices: number[] = []; + const meshIndices: number[] = []; + + gltf.scene.traverse((child: Object3D) => { + if ((child as Mesh).isMesh && (child as Mesh).geometry) { + const mesh = child as Mesh; + const geometry = mesh.geometry as BufferGeometry; + const positionAttribute = geometry.getAttribute( + "position", + ) as BufferAttribute; + const indexArray = geometry.index?.array; + + if (!indexArray) return; + + const baseIndex = meshVertices.length / 3; + + for (let i = 0, l = positionAttribute.count; i < l; i++) { + v.fromBufferAttribute(positionAttribute, i); + v.applyMatrix4(mesh.matrixWorld); + meshVertices.push(v.x, v.y, v.z); + } + + for (let i = 0; i < indexArray.length; i++) { + meshIndices.push(indexArray[i] + baseIndex); + } + } + }); + + const colliderDesc = RAPIER.ColliderDesc.convexDecomposition( + new Float32Array(meshVertices), + new Uint32Array(meshIndices), + ); + + if (colliderDesc) { + const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic(); + const rigidBody = world.createRigidBody(rigidBodyDesc); + world.createCollider(colliderDesc, rigidBody); + rigidBody.setAngvel( + new RAPIER.Vector3( + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + (Math.random() - 0.5) * 2, + ), + true, + ); + + gltf.scene.traverse((child: Object3D) => { + if ((child as Mesh).isMesh) { + const mesh = child as Mesh; + const geometry = mesh.geometry as BufferGeometry; + const vertexColors = new Float32Array( + geometry.attributes.position.count * 3, + ); + const colorPalette = colors[index]; + + for ( + let i = 0; + i < geometry.attributes.position.count; + i++ + ) { + const color = new Color( + colorPalette[ + Math.floor( + (i / geometry.attributes.position.count) * + colorPalette.length, + ) + ], + ); + vertexColors[i * 3] = color.r; + vertexColors[i * 3 + 1] = color.g; + vertexColors[i * 3 + 2] = color.b; + } + + geometry.setAttribute( + "color", + new BufferAttribute(vertexColors, 3), + ); + mesh.material = new MeshPhongMaterial({ + vertexColors: true, + flatShading: false, + }); + } + }); + + testbed.graphics.scene.add(gltf.scene); + } + }); + }); + + for (let i = 0; i < 10; i++) { + const x = (Math.random() - 0.5) * 8; + const y = 3 + Math.random() * 5; + const z = (Math.random() - 0.5) * 8; + const size = 0.3 + Math.random() * 0.4; + + const bodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z); + const body = world.createRigidBody(bodyDesc); + const colliderDesc = RAPIER.ColliderDesc.cuboid(size, size, size); + world.createCollider(colliderDesc, body); + } + + testbed.setWorld(world); + + let cameraPosition = { + eye: {x: 15.0, y: 10.0, z: 15.0}, + target: {x: 0.0, y: 5.0, z: 0.0}, + }; + testbed.lookAt(cameraPosition); +} diff --git a/testbed3d/src/index.ts b/testbed3d/src/index.ts index accfad53..8aef2b9e 100644 --- a/testbed3d/src/index.ts +++ b/testbed3d/src/index.ts @@ -16,11 +16,15 @@ import * as CharacterController from "./demos/characterController"; import * as PidController from "./demos/pidController"; import * as glbToTrimesh from "./demos/glbToTrimesh"; import * as glbToConvexHull from "./demos/glbtoConvexHull"; +import * as CompoundShapes from "./demos/compoundShapes"; +import * as ConvexDecomposition from "./demos/convexDecomposition"; import("@dimforge/rapier3d").then((RAPIER) => { let builders = new Map([ ["collision groups", CollisionGroups.initWorld], ["character controller", CharacterController.initWorld], + ["compound shapes", CompoundShapes.initWorld], + ["convex decomposition", ConvexDecomposition.initWorld], ["convex polyhedron", ConvexPolyhedron.initWorld], ["CCD", CCD.initWorld], ["damping", Damping.initWorld], From c420e59ee205b383c4341a5f20b961e2138029ef Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Sat, 27 Dec 2025 14:59:08 -0500 Subject: [PATCH 4/5] add convex decomposition parameters --- src.ts/geometry/collider.ts | 81 ++++++++++++++++++++++++++++++++++++- src/geometry/shape.rs | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 2 deletions(-) diff --git a/src.ts/geometry/collider.ts b/src.ts/geometry/collider.ts index 20e66726..941be1c8 100644 --- a/src.ts/geometry/collider.ts +++ b/src.ts/geometry/collider.ts @@ -1,4 +1,4 @@ -import {RawColliderSet, RawShape} from "../raw"; +import {RawColliderSet, RawShape, RawVHACDParameters} from "../raw"; import {Rotation, RotationOps, Vector, VectorOps} from "../math"; import { CoefficientCombineRule, @@ -45,6 +45,64 @@ import {ColliderShapeCastHit, ShapeCastHit} from "./toi"; import {ShapeContact} from "./contact"; import {ColliderSet} from "./collider_set"; +/** + * Parameters for VHACD convex decomposition algorithm. + * + * All parameters are optional. When not specified, default values from the + * underlying Rapier/Parry library are used. + */ +export interface VHACDParameters { + /** + * Controls the bias toward clipping along symmetry planes. + * Range: [0.0, 1.0] + */ + alpha?: number; + + /** + * Controls the bias toward clipping along revolution planes. + * Range: [0.0, 1.0] + * Useful for objects with cylindrical or rotational features. + */ + beta?: number; + + /** + * Controls maximum allowed deviation from convexity. + * Range: [0.0, 1.0] + * Lower values create more convex parts for a tighter fit. + */ + concavity?: number; + + /** + * Controls the granularity of searching optimal clipping planes. + * Lower values test more planes (slower but more accurate). + */ + planeDownsampling?: number; + + /** + * Controls the precision of convex hull generation. + * Lower values use more voxels for hull computation. + */ + convexHullDownsampling?: number; + + /** + * Maximum number of convex parts to generate. + * Limits part count during decomposition. + */ + maxConvexHulls?: number; + + /** + * Voxel grid resolution for decomposition. + * Higher resolution captures more detail but is slower. + */ + resolution?: number; + + /** + * Whether to use approximate convex hulls for better performance. + * Faster method with minimal impact on quality. + */ + convexHullApproximation?: boolean; +} + /** * Flags affecting whether collision-detection happens between two colliders * depending on the type of rigid-bodies they are attached to. @@ -1713,12 +1771,31 @@ export class ColliderDesc { * * @param vertices - The coordinates of the mesh's vertices. * @param indices - The indices of the mesh's triangles. + * @param params - Optional VHACD parameters to control the decomposition. */ public static convexDecomposition( vertices: Float32Array, indices: Uint32Array, + params?: VHACDParameters, ): ColliderDesc | null { - const rawShape = RawShape.convexDecomposition(vertices, indices); + let rawShape: RawShape; + + if (params) { + // Convert TypeScript params to Rust params + const rawParams = new RawVHACDParameters(); + if (params.alpha !== undefined) rawParams.alpha = params.alpha; + if (params.beta !== undefined) rawParams.beta = params.beta; + if (params.concavity !== undefined) rawParams.concavity = params.concavity; + if (params.planeDownsampling !== undefined) rawParams.plane_downsampling = params.planeDownsampling; + if (params.convexHullDownsampling !== undefined) rawParams.convex_hull_downsampling = params.convexHullDownsampling; + if (params.maxConvexHulls !== undefined) rawParams.max_convex_hulls = params.maxConvexHulls; + if (params.resolution !== undefined) rawParams.resolution = params.resolution; + if (params.convexHullApproximation !== undefined) rawParams.convex_hull_approximation = params.convexHullApproximation; + + rawShape = RawShape.convexDecompositionWithParams(vertices, indices, rawParams); + } else { + rawShape = RawShape.convexDecomposition(vertices, indices); + } if (!rawShape) { return null; diff --git a/src/geometry/shape.rs b/src/geometry/shape.rs index 27f97cec..ac09f91a 100644 --- a/src/geometry/shape.rs +++ b/src/geometry/shape.rs @@ -9,6 +9,8 @@ use rapier::geometry::{Shape, SharedShape, TriMeshFlags}; use rapier::math::{Isometry, Point, Real, Vector, DIM}; use rapier::parry::query; use rapier::parry::query::{Ray, ShapeCastOptions}; +#[cfg(any(feature = "dim2", feature = "dim3"))] +use rapier::parry::transformation::vhacd::VHACDParameters; use wasm_bindgen::prelude::*; pub trait SharedShapeUtility { @@ -224,6 +226,58 @@ pub enum RawShapeType { Voxels = 18, } +/// Parameters for VHACD convex decomposition algorithm +#[wasm_bindgen] +pub struct RawVHACDParameters(pub(crate) VHACDParameters); + +#[wasm_bindgen] +impl RawVHACDParameters { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + RawVHACDParameters(VHACDParameters::default()) + } + + #[wasm_bindgen(getter)] + pub fn alpha(&self) -> f32 { self.0.alpha } + #[wasm_bindgen(setter)] + pub fn set_alpha(&mut self, val: f32) { self.0.alpha = val.clamp(0.0, 1.0); } + + #[wasm_bindgen(getter)] + pub fn beta(&self) -> f32 { self.0.beta } + #[wasm_bindgen(setter)] + pub fn set_beta(&mut self, val: f32) { self.0.beta = val.clamp(0.0, 1.0); } + + #[wasm_bindgen(getter)] + pub fn concavity(&self) -> f32 { self.0.concavity } + #[wasm_bindgen(setter)] + pub fn set_concavity(&mut self, val: f32) { self.0.concavity = val.clamp(0.0, 1.0); } + + #[wasm_bindgen(getter)] + pub fn plane_downsampling(&self) -> u32 { self.0.plane_downsampling } + #[wasm_bindgen(setter)] + pub fn set_plane_downsampling(&mut self, val: u32) { self.0.plane_downsampling = val; } + + #[wasm_bindgen(getter)] + pub fn convex_hull_downsampling(&self) -> u32 { self.0.convex_hull_downsampling } + #[wasm_bindgen(setter)] + pub fn set_convex_hull_downsampling(&mut self, val: u32) { self.0.convex_hull_downsampling = val; } + + #[wasm_bindgen(getter)] + pub fn max_convex_hulls(&self) -> u32 { self.0.max_convex_hulls } + #[wasm_bindgen(setter)] + pub fn set_max_convex_hulls(&mut self, val: u32) { self.0.max_convex_hulls = val; } + + #[wasm_bindgen(getter)] + pub fn resolution(&self) -> u32 { self.0.resolution } + #[wasm_bindgen(setter)] + pub fn set_resolution(&mut self, val: u32) { self.0.resolution = val; } + + #[wasm_bindgen(getter)] + pub fn convex_hull_approximation(&self) -> bool { self.0.convex_hull_approximation } + #[wasm_bindgen(setter)] + pub fn set_convex_hull_approximation(&mut self, val: bool) { self.0.convex_hull_approximation = val; } +} + #[wasm_bindgen] pub struct RawShape(pub(crate) SharedShape); @@ -451,6 +505,19 @@ impl RawShape { Some(Self(shape)) } + #[cfg(feature = "dim3")] + pub fn convexDecompositionWithParams( + vertices: Vec, + indices: Vec, + params: &RawVHACDParameters + ) -> Option { + 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, ¶ms.0); + Some(Self(shape)) + } + #[cfg(feature = "dim2")] pub fn convexDecomposition(vertices: Vec, indices: Vec) -> Option { let vertices: Vec<_> = vertices.chunks(DIM).map(|v| Point::from_slice(v)).collect(); @@ -460,6 +527,19 @@ impl RawShape { Some(Self(shape)) } + #[cfg(feature = "dim2")] + pub fn convexDecompositionWithParams( + vertices: Vec, + indices: Vec, + params: &RawVHACDParameters + ) -> Option { + 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, ¶ms.0); + Some(Self(shape)) + } + pub fn castShape( &self, shapePos1: &RawVector, From 8ef99a6e758dafa191b690a3db7e89afb9fb9641 Mon Sep 17 00:00:00 2001 From: Harshal Sheth Date: Sat, 27 Dec 2025 15:04:56 -0500 Subject: [PATCH 5/5] code formatting --- src.ts/geometry/collider.ts | 30 ++++++--- src/geometry/shape.rs | 74 ++++++++++++++++------ testbed3d/src/demos/compoundShapes.ts | 21 +++--- testbed3d/src/demos/convexDecomposition.ts | 4 +- 4 files changed, 87 insertions(+), 42 deletions(-) diff --git a/src.ts/geometry/collider.ts b/src.ts/geometry/collider.ts index 941be1c8..d7b90e4a 100644 --- a/src.ts/geometry/collider.ts +++ b/src.ts/geometry/collider.ts @@ -1779,20 +1779,32 @@ export class ColliderDesc { params?: VHACDParameters, ): ColliderDesc | null { let rawShape: RawShape; - + if (params) { // Convert TypeScript params to Rust params const rawParams = new RawVHACDParameters(); if (params.alpha !== undefined) rawParams.alpha = params.alpha; if (params.beta !== undefined) rawParams.beta = params.beta; - if (params.concavity !== undefined) rawParams.concavity = params.concavity; - if (params.planeDownsampling !== undefined) rawParams.plane_downsampling = params.planeDownsampling; - if (params.convexHullDownsampling !== undefined) rawParams.convex_hull_downsampling = params.convexHullDownsampling; - if (params.maxConvexHulls !== undefined) rawParams.max_convex_hulls = params.maxConvexHulls; - if (params.resolution !== undefined) rawParams.resolution = params.resolution; - if (params.convexHullApproximation !== undefined) rawParams.convex_hull_approximation = params.convexHullApproximation; - - rawShape = RawShape.convexDecompositionWithParams(vertices, indices, rawParams); + if (params.concavity !== undefined) + rawParams.concavity = params.concavity; + if (params.planeDownsampling !== undefined) + rawParams.plane_downsampling = params.planeDownsampling; + if (params.convexHullDownsampling !== undefined) + rawParams.convex_hull_downsampling = + params.convexHullDownsampling; + if (params.maxConvexHulls !== undefined) + rawParams.max_convex_hulls = params.maxConvexHulls; + if (params.resolution !== undefined) + rawParams.resolution = params.resolution; + if (params.convexHullApproximation !== undefined) + rawParams.convex_hull_approximation = + params.convexHullApproximation; + + rawShape = RawShape.convexDecompositionWithParams( + vertices, + indices, + rawParams, + ); } else { rawShape = RawShape.convexDecomposition(vertices, indices); } diff --git a/src/geometry/shape.rs b/src/geometry/shape.rs index ac09f91a..37933da0 100644 --- a/src/geometry/shape.rs +++ b/src/geometry/shape.rs @@ -238,44 +238,76 @@ impl RawVHACDParameters { } #[wasm_bindgen(getter)] - pub fn alpha(&self) -> f32 { self.0.alpha } + pub fn alpha(&self) -> f32 { + self.0.alpha + } #[wasm_bindgen(setter)] - pub fn set_alpha(&mut self, val: f32) { self.0.alpha = val.clamp(0.0, 1.0); } + pub fn set_alpha(&mut self, val: f32) { + self.0.alpha = val.clamp(0.0, 1.0); + } #[wasm_bindgen(getter)] - pub fn beta(&self) -> f32 { self.0.beta } + pub fn beta(&self) -> f32 { + self.0.beta + } #[wasm_bindgen(setter)] - pub fn set_beta(&mut self, val: f32) { self.0.beta = val.clamp(0.0, 1.0); } + pub fn set_beta(&mut self, val: f32) { + self.0.beta = val.clamp(0.0, 1.0); + } #[wasm_bindgen(getter)] - pub fn concavity(&self) -> f32 { self.0.concavity } + pub fn concavity(&self) -> f32 { + self.0.concavity + } #[wasm_bindgen(setter)] - pub fn set_concavity(&mut self, val: f32) { self.0.concavity = val.clamp(0.0, 1.0); } + pub fn set_concavity(&mut self, val: f32) { + self.0.concavity = val.clamp(0.0, 1.0); + } #[wasm_bindgen(getter)] - pub fn plane_downsampling(&self) -> u32 { self.0.plane_downsampling } + pub fn plane_downsampling(&self) -> u32 { + self.0.plane_downsampling + } #[wasm_bindgen(setter)] - pub fn set_plane_downsampling(&mut self, val: u32) { self.0.plane_downsampling = val; } + pub fn set_plane_downsampling(&mut self, val: u32) { + self.0.plane_downsampling = val; + } #[wasm_bindgen(getter)] - pub fn convex_hull_downsampling(&self) -> u32 { self.0.convex_hull_downsampling } + pub fn convex_hull_downsampling(&self) -> u32 { + self.0.convex_hull_downsampling + } #[wasm_bindgen(setter)] - pub fn set_convex_hull_downsampling(&mut self, val: u32) { self.0.convex_hull_downsampling = val; } + pub fn set_convex_hull_downsampling(&mut self, val: u32) { + self.0.convex_hull_downsampling = val; + } #[wasm_bindgen(getter)] - pub fn max_convex_hulls(&self) -> u32 { self.0.max_convex_hulls } + pub fn max_convex_hulls(&self) -> u32 { + self.0.max_convex_hulls + } #[wasm_bindgen(setter)] - pub fn set_max_convex_hulls(&mut self, val: u32) { self.0.max_convex_hulls = val; } + pub fn set_max_convex_hulls(&mut self, val: u32) { + self.0.max_convex_hulls = val; + } #[wasm_bindgen(getter)] - pub fn resolution(&self) -> u32 { self.0.resolution } + pub fn resolution(&self) -> u32 { + self.0.resolution + } #[wasm_bindgen(setter)] - pub fn set_resolution(&mut self, val: u32) { self.0.resolution = val; } + pub fn set_resolution(&mut self, val: u32) { + self.0.resolution = val; + } #[wasm_bindgen(getter)] - pub fn convex_hull_approximation(&self) -> bool { self.0.convex_hull_approximation } + pub fn convex_hull_approximation(&self) -> bool { + self.0.convex_hull_approximation + } #[wasm_bindgen(setter)] - pub fn set_convex_hull_approximation(&mut self, val: bool) { self.0.convex_hull_approximation = val; } + pub fn set_convex_hull_approximation(&mut self, val: bool) { + self.0.convex_hull_approximation = val; + } } #[wasm_bindgen] @@ -473,7 +505,8 @@ impl RawShape { positions[pos_offset], positions[pos_offset + 1], positions[pos_offset + 2], - ).into(); + ) + .into(); #[cfg(feature = "dim2")] let rotation = na::UnitComplex::new(rotations[i]); @@ -509,7 +542,7 @@ impl RawShape { pub fn convexDecompositionWithParams( vertices: Vec, indices: Vec, - params: &RawVHACDParameters + params: &RawVHACDParameters, ) -> Option { 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(); @@ -523,7 +556,8 @@ impl 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()); + let shape = + SharedShape::convex_decomposition_with_params(&vertices, &indices, &Default::default()); Some(Self(shape)) } @@ -531,7 +565,7 @@ impl RawShape { pub fn convexDecompositionWithParams( vertices: Vec, indices: Vec, - params: &RawVHACDParameters + params: &RawVHACDParameters, ) -> Option { 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(); diff --git a/testbed3d/src/demos/compoundShapes.ts b/testbed3d/src/demos/compoundShapes.ts index 1e821c1b..57ea636e 100644 --- a/testbed3d/src/demos/compoundShapes.ts +++ b/testbed3d/src/demos/compoundShapes.ts @@ -76,9 +76,9 @@ function createStairsShape(RAPIER: RAPIER_API): any { } function createDumbbellShape(RAPIER: RAPIER_API): any { - const shape1 = new RAPIER.Ball(0.8); // left weight + const shape1 = new RAPIER.Ball(0.8); // left weight const shape2 = new RAPIER.Cuboid(1.5, 0.2, 0.2); // bar - const shape3 = new RAPIER.Ball(0.8); // right weight + const shape3 = new RAPIER.Ball(0.8); // right weight const shapes = [shape1, shape2, shape3]; const positions = [ @@ -109,7 +109,11 @@ export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { for (let i = 0; i < 3; i++) { const x = (i - 1) * 8; - const bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation(x, 1.0, 0.0); + const bodyDesc = RAPIER.RigidBodyDesc.fixed().setTranslation( + x, + 1.0, + 0.0, + ); const body = world.createRigidBody(bodyDesc); const colliderDesc = RAPIER.ColliderDesc.cylinder(2.0, 0.5); world.createCollider(colliderDesc, body); @@ -155,11 +159,7 @@ export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { true, ); body.setLinvel( - new RAPIER.Vector3( - (rng() - 0.5) * 2, - -1.0, - (rng() - 0.5) * 2, - ), + new RAPIER.Vector3((rng() - 0.5) * 2, -1.0, (rng() - 0.5) * 2), true, ); } @@ -176,10 +176,7 @@ export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { const colliderDesc = createDumbbellShape(RAPIER); world.createCollider(colliderDesc, body); body.setLinvel(new RAPIER.Vector3(side * -1.5, 0.0, 0.0), true); - body.setAngvel( - new RAPIER.Vector3(0.0, (rng() - 0.5) * 4, 0.0), - true, - ); + body.setAngvel(new RAPIER.Vector3(0.0, (rng() - 0.5) * 4, 0.0), true); } testbed.setWorld(world); diff --git a/testbed3d/src/demos/convexDecomposition.ts b/testbed3d/src/demos/convexDecomposition.ts index a9bc79bf..9d627708 100644 --- a/testbed3d/src/demos/convexDecomposition.ts +++ b/testbed3d/src/demos/convexDecomposition.ts @@ -108,7 +108,9 @@ export function initWorld(RAPIER: RAPIER_API, testbed: Testbed) { const color = new Color( colorPalette[ Math.floor( - (i / geometry.attributes.position.count) * + (i / + geometry.attributes.position + .count) * colorPalette.length, ) ],