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:

let body = RigidBodyBuilder::dynamic()
.translation(Vec2::new(0.0, 3.0))
.rotation(0.3)
.linvel(Vec2::new(1.0, 0.0))
.angvel(1.0)
.build();
let collider = ColliderBuilder::ball(0.5).density(2.0).build();
let 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:

let body = RigidBodyBuilder::dynamic()
.translation(Vec2::new(0.0, 3.0))
.build();
let parent = state.insert_body(body, RbdCoupling::None);
for x in [-0.5, 0.5] {
let collider = ColliderBuilder::ball(0.5)
.translation(Vec2::new(x, 0.0))
.density(1.0)
.friction(0.8)
.restitution(0.3)
.build();
state.insert_collider_in(0, collider, Some(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. ColliderBuilder::friction_combine_rule and ColliderBuilder::restitution_combine_rule also transfer their CoefficientCombineRule to the GPU.

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_with_flags in 3D and ColliderBuilder::polyline in 2D.

For a 3D triangle mesh, pass TriMeshFlags::ORIENTED to ColliderBuilder::trimesh_with_flags. Nexus requires the resulting pseudo-normals; a mesh without them is rejected. 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_with_flags in 3D or ColliderBuilder::polyline in 2D.

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:

let groups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_2, InteractionTestMode::And);
let 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​

RbdSimParams, imported from nexus2d::rbd::dynamics or nexus3d::rbd::dynamics, describes a rigid-body step. Call state.set_rbd_sim_params(env, params) before state.finalize:

let params = RbdSimParams {
dt: 1.0 / 120.0,
num_solver_iterations: 8,
..RbdSimParams::default()
};
state.set_rbd_sim_params(0, params);
state.set_rbd_steps_per_frame(2);

Here, params.dt 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 params.num_solver_iterations field controls the TGS substeps within each rigid-body step; its default is 4. Increasing it can improve contacts and joint constraints at the expense of computations. Calling state.set_rbd_sim_params marks the scene for rebuilding, so configure it before starting.

RbdSimParams also exposes length_unit, normalized_allowed_linear_error, normalized_prediction_distance, and normalized_max_corrective_velocity. Keep their defaults at first: setting all tolerances to zero doesn't make contacts exact, and aggressive correction can cause jitter. All batch environments must use identical RbdSimParams values.

After state.finalize, use state.set_rbd_gravity(backend, [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_friction_model(FrictionModel::Simplified);

The model applies to every environment. state.set_rbd_friction_model can also be called on a running simulation: body poses and velocities are kept, and only the contact warmstarts restart.

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.