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