Joints and multibodies
Nexus accepts Rapier's joint builders for articulated mechanisms.
Refer to that guide for the joint types, local anchors, limits, and Degrees Of Freedom (DOF).
Nexus provides state.insert_impulse_joint in 2D and 3D, and state.insert_multibody_joint for its 3D
reduced-coordinates solver. The difference between the two approaches is described in
Rapier's joint constraints.
Creating an impulse joint
Call state.insert_impulse_joint(body1, body2, joint) with two existing
RigidBodyHandle values from the same
environment. The following uses
RevoluteJointBuilder::new:
- Example 2D
- Example 3D
let joint = RevoluteJointBuilder::new()
.local_anchor1(Vec2::new(0.0, 0.0))
.local_anchor2(Vec2::new(0.0, 1.0))
.limits([-1.0, 1.0])
.motor_velocity(1.0, 0.5)
.motor_max_force(10.0)
.build();
state.insert_impulse_joint(body1, body2, joint);
let joint = RevoluteJointBuilder::new(Vec3::Z)
.local_anchor1(Vec3::new(0.0, 0.0, 0.0))
.local_anchor2(Vec3::new(0.0, 1.0, 0.0))
.limits([-1.0, 1.0])
.motor_velocity(1.0, 0.5)
.motor_max_force(10.0)
.build();
state.insert_impulse_joint(body1, body2, joint);
The RevoluteJointBuilder::local_anchor1 and RevoluteJointBuilder::local_anchor2
arguments are local to body1 and body2 respectively.
RevoluteJointBuilder::limits([-1.0, 1.0])
configures the limits, and RevoluteJointBuilder::motor_velocity and RevoluteJointBuilder::motor_max_force
configure the initial motor. See those sections for the units and motor behavior.
In 2D, RevoluteJointBuilder::new() doesn't take an axis.
In 3D, RevoluteJointBuilder::new(Vec3::Z) selects the
local rotation axis.
The joint builder only sets the initial target. Use the runtime APIs below for continuous motor control.
If an impulse joint drifts,
increase RbdSimParams::num_solver_iterations before state.finalize, or consider state.insert_multibody_joint
for a 3D tree.
For an object whose parts never move wrt. each other, state.insert_collider_in can attach several
colliders to one body without adding joints.
Creating a multibody
Call state.insert_multibody_joint to add a reduced-coordinate joint.
The following 3D pendulum uses a fixed base and a moving link. The base doesn't need a collider:
let base = state.insert_body(RigidBodyBuilder::fixed().build(), RbdCoupling::None);
let body = RigidBodyBuilder::dynamic()
.translation(Vec3::new(0.0, -1.0, 0.0))
.build();
let link = state.insert_rigid_body(body, ColliderBuilder::ball(0.3).build(), RbdCoupling::None);
let joint = RevoluteJointBuilder::new(Vec3::Z)
.local_anchor2(Vec3::Y)
.motor_velocity(0.0, 1.0)
.motor_max_force(10.0)
.build();
let joint_handle = state
.insert_multibody_joint(base, link, joint)
.expect("The joint must not create a cycle.");
The state.insert_multibody_joint return value is None if the joint would create an invalid chain, e.g., a cycle.
Always check that insertion succeeded before using the returned handle. A multibody
must have a tree structure. state.insert_impulse_joint can add a remaining loop-closing constraint, but that
constraint is still solved iteratively and can exhibit drift.
Motor control
Call state.finalize before sending runtime motor targets.
state.control_multibody_motors gives a callback access to each environment's authored
multibody joints, then uploads the changed
motor settings without rebuilding the scene. The following uses joint_handle from the previous example:
state.control_multibody_motors(backend, |_, world| {
let (multibody, link_id) = world.multibody_joints.get_mut(joint_handle).unwrap();
multibody
.link_mut(link_id)
.unwrap()
.joint
.data
.set_motor_velocity(JointAxis::AngX, 1.0, 1.0);
})?;
The JointAxis::AngX
value selects the free joint coordinate. Its geometric rotation axis still comes from
RevoluteJointBuilder::new,
so AngX doesn't mean the world x axis here.
In a batch, select the appropriate joint handle for the callback's environment. The handle in this example
belongs to environment 0.
Reading joint state
Call state.read_multibody_links to retrieve generalized coordinates, poses, and velocities.
The returned vector is ordered by environment, then by GPU traversal order.
Parent links precede their children. This isn't the integer order of
RigidBodyHandle values.
Link velocities are meaningful after the first pipeline.simulate call.
Loading robots
The repository contains complete URDF
and MJCF examples.
The loaders populate the PhysicsWorld
returned by state.rbd_world_mut(env). Register graphics with viewer.insert_visual_shape, then call state.finalize.
For continuous actuation, call state.control_multibody_motors; calling state.rbd_world_mut every frame would
mark the scene for rebuilding.
Keep state.set_rbd_gravity consistent with the robot model's up axis.
Changing the camera doesn't change gravity.