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:
- Example 2D
- Example 3D
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);
let body = RigidBodyBuilder::dynamic()
.translation(Vec3::new(0.0, 3.0, 0.0))
.rotation(Vec3::new(0.0, 0.0, 0.3))
.linvel(Vec3::new(1.0, 0.0, 0.0))
.angvel(Vec3::new(0.0, 0.0, 1.0))
.build();
let collider = ColliderBuilder::ball(0.5).density(2.0).build();
let handle = state.insert_rigid_body(body, collider, RbdCoupling::None);
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::NoneRbdCoupling.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:
RigidBodyBuilder::dynamicandRigidBodyBuilder::fixedRigidBodyBuilder.dynamicandRigidBodyBuilder.fixedselect the body type.RigidBodyBuilder::translationRigidBodyBuilder.translationandRigidBodyBuilder::rotationRigidBodyBuilder.rotationset the initial position.RigidBodyBuilder::linvelRigidBodyBuilder.linvelandRigidBodyBuilder::angvelRigidBodyBuilder.angvelset the initial velocities.ColliderBuilder::densityColliderBuilder.density,ColliderBuilder::massColliderBuilder.mass, andRigidBodyBuilder::additional_massRigidBodyBuilder.additional_masssupply the mass properties used duringstate.finalize.
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:
- Example 2D
- Example 3D
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));
}
let body = RigidBodyBuilder::dynamic()
.translation(Vec3::new(0.0, 3.0, 0.0))
.build();
let parent = state.insert_body(body, RbdCoupling::None);
for x in [-0.5, 0.5] {
let collider = ColliderBuilder::ball(0.5)
.translation(Vec3::new(x, 0.0, 0.0))
.density(1.0)
.friction(0.8)
.restitution(0.3)
.build();
state.insert_collider_in(0, collider, Some(parent));
}
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::translationColliderBuilder.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::frictionColliderBuilder.friction and ColliderBuilder::restitutionColliderBuilder.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.
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_yColliderBuilder.ball, ColliderBuilder.cuboid, or ColliderBuilder.capsule_y.
Their dimensions and constructors are described in the Rapier guide. Nexus also supports
ColliderBuilder::convex_hullColliderBuilder.convex_hull,
ColliderBuilder::trimesh_with_flags in 3D and ColliderBuilder::polyline in 2DColliderBuilder.trimesh.
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.
ColliderBuilder.trimesh enables TriMeshFlags::ORIENTED
internally. Supply consistently wound triangles so their normals point outward.
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 2DColliderBuilder.trimesh.
Collision filtering
Nexus transfers ColliderBuilder::collision_groupsColliderBuilder.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:
- Example 2D
- Example 3D
let groups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_2, InteractionTestMode::And);
let collider = ColliderBuilder::ball(0.5).collision_groups(groups).build();
let groups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_2, InteractionTestMode::And);
let collider = ColliderBuilder::ball(0.5).collision_groups(groups).build();
groups = InteractionGroups.new(0b0001, 0b0010)
collider = ColliderBuilder.ball(0.5).collision_groups(groups).build()
Use ColliderBuilder::collision_groupsColliderBuilder.collision_groups
for supported filtering. Nexus's high-level pipeline doesn't provide Rapier's
ColliderBuilder::solver_groupsColliderBuilder.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:
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:
- Example 2D
- Example 3D
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);
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);
state.set_rbd_timestep(1.0 / 120.0, 8)
state.set_rbd_steps_per_frame(2)
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.
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.
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])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::densityRigidBodyBuilder.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);
state.set_rbd_solver_params(friction_model="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.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::fixedRigidBodyBuilder.dynamic
and RigidBodyBuilder.fixed for the scenes shown here.