Skip to main content

Rigid-bodies and colliders

Nexus uses Rapier's RigidBodyBuilder and ColliderBuilder to describe the scene. Refer to those pages for the rigid-body and collider concepts. This page explains their insertion into NexusState, the settings transferred to Nexus, and the configuration of the Nexus rigid-body solver.

Creation and insertion​

Create a body and collider with RigidBodyBuilder and ColliderBuilder, then call state.insert_rigid_body(body, collider, coupling). The returned handle identifies the body in environment 0. The following inserts a body into an existing NexusState:

body = (RigidBodyBuilder.dynamic()
.translation(Vec3(0.0, 3.0, 0.0))
.rotation(Vec3(0.0, 0.0, 0.3))
.linvel(Vec3(1.0, 0.0, 0.0))
.angvel(Vec3(0.0, 0.0, 1.0))
.build())
collider = ColliderBuilder.ball(0.5).density(2.0).build()
handle = state.insert_rigid_body(body, collider, RbdCoupling.NONE)

The RbdCoupling.NONE argument selects ordinary rigid-body dynamics. The other RbdCoupling values are described in the MPM coupling page. Call state.finalize before the first viewer.sync, or let pipeline.simulate finalize the scene automatically.

For the initial state, use the following Rapier APIs:

These builder methods configure the initial scene. To retrieve a simulated position, use readback.

Several colliders on one body​

Call state.insert_body to insert a body without a collider, then call state.insert_collider_in for each Collider, using the same parent handle:

body = RigidBodyBuilder.dynamic().translation(Vec3(0.0, 3.0, 0.0)).build()
parent = state.insert_body(body, RbdCoupling.NONE)
for x in [-0.5, 0.5]:
collider = (ColliderBuilder.ball(0.5)
.translation(Vec3(x, 0.0, 0.0))
.density(1.0).friction(0.8).restitution(0.3).build())
state.insert_collider_in(0, collider, parent)

The first argument of state.insert_collider_in is the environment index. Passing None as the parent creates a parentless Collider in the fixed environment. The ColliderBuilder.translation values above are local to the parent body. Pass the same local Pose to viewer.insert_shape, otherwise graphics and contacts won't match.

Nexus uploads the ColliderBuilder.friction and ColliderBuilder.restitution coefficients. See Rapier's restitution section for restitution and its combine rule.

The Python ColliderBuilder doesn't expose combine-rule setters; the default CoefficientCombineRule::Average applies.

Supported shapes​

Start with ColliderBuilder.ball, ColliderBuilder.cuboid, or ColliderBuilder.capsule_y. Their dimensions and constructors are described in the Rapier guide. Nexus also supports ColliderBuilder.convex_hull, ColliderBuilder.trimesh.

ColliderBuilder.trimesh enables TriMeshFlags::ORIENTED internally. Supply consistently wound triangles so their normals point outward.

warning

The GPU conversion doesn't implement Rapier's heightfields or directly accept its compound shapes. For a non-convex dynamic object, call state.insert_collider_in several times as shown above. For terrain, use ColliderBuilder.trimesh.

Collision filtering​

Nexus transfers ColliderBuilder.collision_groups and InteractionGroups to the compute buffers. Refer to that section for the membership/filter test. The following creates a collider that belongs to the first group and accepts the second group:

groups = InteractionGroups.new(0b0001, 0b0010)
collider = ColliderBuilder.ball(0.5).collision_groups(groups).build()

Use ColliderBuilder.collision_groups for supported filtering. Nexus's high-level pipeline doesn't provide Rapier's ColliderBuilder.solver_groups, sensor events, or physics hooks.

Integration parameters​

state.set_rbd_timestep(dt, substeps) sets the rigid-body timestep and the number of TGS substeps of every environment, and state.set_rbd_steps_per_frame selects how many complete rigid-body steps pipeline.simulate executes. Call them before state.finalize:

state.set_rbd_timestep(1.0 / 120.0, 8)
state.set_rbd_steps_per_frame(2)

Here, the timestep is 1 / 120 seconds and state.set_rbd_steps_per_frame(2) selects two steps, so one call to pipeline.simulate advances 1 / 60 seconds. The second argument of state.set_rbd_timestep is the number of TGS substeps within each rigid-body step; its default is 4. Increasing it can improve contacts and joint constraints at the expense of computations. The default timestep is 1 / 60 seconds. state.set_rbd_solver_params changes the contact tolerances and solver options through keyword arguments, and state.rbd_solver_params() returns their current values.

All batch environments must use identical RbdSimParams values.

After state.finalize, use state.set_rbd_gravity(viewer, Vec3(0.0, -9.81, 0.0)) to change gravity. The Rust setter takes a three-element array even in 2D, where its third component is ignored. Calling state.set_rbd_gravity before state.finalize has no effect because the rigid-body buffers don't exist yet.

If a body doesn't fall, check its RigidBodyBuilder.dynamic and ColliderBuilder.density configuration, then the position of state.set_rbd_gravity in the initialization sequence. Use consistent units and object sizes, as discussed in Rapier's common mistakes. If motion appears slow, also check how often the render loop calls pipeline.simulate: a fixed timestep doesn't adapt automatically to the rendering frame rate.

Friction model​

In 3D, the friction model selects how contacts resist sliding. The default Coulomb model solves friction at each contact point. The simplified model solves a single friction constraint at the center of each contact manifold, plus a twist constraint resisting rotation around the contact normal. It solves fewer constraints, which helps scenes with many resting contacts. Multibody contacts always use the Coulomb model:

state.set_rbd_solver_params(friction_model="simplified")

The model applies to every environment. Call state.set_rbd_solver_params before state.finalize.

Other Rapier settings​

Nexus uses its own NexusPipeline, and doesn't run Rapier's PhysicsPipeline. In particular, Rapier's sleeping, CCD, dominance, damping, gravity_scale, and RigidBody::add_force and RigidBody::apply_impulse are not transferred to the ordinary GPU rigid-body solver. The presence of a method on RigidBodyBuilder is not a guarantee that Nexus implements it.

Keep the same distinction for kinematic bodies: Nexus doesn't expose the per-frame RigidBody::set_next_kinematic_position workflow. Start with RigidBodyBuilder.dynamic and RigidBodyBuilder.fixed for the scenes shown here.