Skip to main content

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::newRevoluteJointBuilder.new:

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);
joint = (RevoluteJointBuilder.new(Vec3.Z)
.local_anchor1(Vec3(0.0, 0.0, 0.0))
.local_anchor2(Vec3(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_anchor1RevoluteJointBuilder.local_anchor1 and RevoluteJointBuilder::local_anchor2RevoluteJointBuilder.local_anchor2 arguments are local to body1 and body2 respectively. RevoluteJointBuilder::limits([-1.0, 1.0])RevoluteJointBuilder.limits(-1.0, 1.0) configures the limits, and RevoluteJointBuilder::motor_velocityRevoluteJointBuilder.motor_velocity and RevoluteJointBuilder::motor_max_forceRevoluteJointBuilder.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)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.consider state.insert_multibody_joint for a 3D tree; changing RbdSimParams::num_solver_iterations requires the Rust API. 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.");
base = state.insert_body(RigidBodyBuilder.fixed().build(), RbdCoupling.NONE)
body = RigidBodyBuilder.dynamic().translation(Vec3(0.0, -1.0, 0.0)).build()
link = state.insert_rigid_body(body, ColliderBuilder.ball(0.3).build(), RbdCoupling.NONE)
joint = (RevoluteJointBuilder.new(Vec3.Z).local_anchor2(Vec3.Y)
.motor_velocity(0.0, 1.0).motor_max_force(10.0).build())
joint_handle = state.insert_multibody_joint(base, link, joint)
assert joint_handle is not None, "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.set_multibody_motor_velocity takes an environment index, a GPU link index, and JointAxis. The GPU link index is not a RigidBodyHandle. In the pendulum above, base is link 0 and link is link 1:

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);
})?;
# In this two-link example the base is link 0 and the pendulum is link 1.
state.set_multibody_motor_velocity(viewer, 0, 1, JointAxis.AngX, 1.0)

The JointAxis::AngXJointAxis.AngX value selects the free joint coordinate. Its geometric rotation axis still comes from RevoluteJointBuilder::newRevoluteJointBuilder.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. The returned NumPy arrays use the same order. See the readback example for the array layout. 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. state.insert_urdf(path, options) returns render shapes for viewer.insert_visual_shape. The state.insert_mjcf(viewer, scene_path) method registers graphics itself. After state.finalize, state.apply_actuator_controls(viewer, ctrl) applies a control vector in state.actuator_names() order. The Python examples show the complete sequence.

Keep state.set_rbd_gravity consistent with the robot model's up axis. For a Z-up scene, call viewer.set_up_axis(Vec3.Z) and set state.set_rbd_gravity to Vec3(0.0, 0.0, -9.81) after state.finalize. Changing the camera doesn't change gravity.