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

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_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.

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, 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:

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

# 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.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.

Reading joint state​

Call state.read_multibody_links to retrieve generalized coordinates, poses, and velocities.

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.

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.