Skip to main content

Reading and modifying rigid-bodies

After state.finalize, the running rigid-body simulation is stored in compute buffers. The original RigidBody values describe the initial scene. This page explains how to retrieve results and modify NexusState without accidentally rebuilding that scene. MPM updates are covered separately by particle chunk operations.

Reading positions​

Call state.read_multibody_links for the links of a 3D multibody. This method needs NumPy (python -m pip install numpy). The following assumes that the scene contains the pendulum:

coords, positions, quats, linvels, angvels = state.read_multibody_links(viewer)
stride = state.multibody_links_per_env()
positions_env0 = positions[:stride]
print("Link positions:", positions_env0)

The positions, linvels, and angvels arrays have three components per row. The quats rows contain (w, x, y, z). Each coords row has six slots, of which only the joint's DOF count is meaningful; the revolute coordinate is stored in slot 5. The arrays are ordered by environment, then by link. state.multibody_links_per_env() returns the environment stride. The Python bindings don't currently provide an equivalent general-purpose pose readback for ordinary rigid-bodies.

info

state.read_multibody_links can wait for previously submitted compute work and copy data to the CPU. Call it at the frequency your application needs. Reading every small physics substep can be expensive. For rendering alone, viewer.sync handles synchronization.

If a position never changes, check that it came from the readback above, instead of calling RigidBody.translation on the original scene description. Also check that the loop reaches pipeline.simulate before reading the result.

Changing the scene description​

The state.insert_rigid_body and state.insert_impulse_joint methods modify the authored scene and mark it for a rebuild. The next state.finalize, including the one called automatically by pipeline.simulate, rebuilds the rigid-body buffers from that description.

warning

Calling state.finalize after such edits can restore bodies to their authored positions. Use state.insert_rigid_body for initial construction, and the runtime APIs below when the current simulated state must be preserved.

Inserting bodies while running​

For a single environment containing primitive shapes, call state.reserve_rigid_bodies before the first state.finalize. Then state.add_rigid_bodies can append bodies directly to the running buffers while the reserved space remains:

state = NexusState()
state.reserve_rigid_bodies(128)
ground = RigidBodyBuilder.fixed().build()
collider = ColliderBuilder.cuboid(5.0, 0.1, 5.0).build()
state.insert_rigid_body(ground, collider, RbdCoupling.NONE)
state.finalize(viewer)

body = RigidBodyBuilder.dynamic().translation(Vec3(0.0, 3.0, 0.0)).build()
state.add_rigid_bodies(viewer, [body], [ColliderBuilder.ball(0.5).build()], RbdCoupling.NONE)

The state.reserve_rigid_bodies(128) call reserves 128 collider slots in total, including the ground. This direct insertion path requires a single environment and primitive, vertex-less colliders. It doesn't support mesh colliders, multibodies, or arbitrary changes to a batch.

If state.finalize hasn't been called, or the reserved space cannot fit the complete insertion, state.add_rigid_bodies fall back to ordinary insertion and a subsequent rebuild. Reserve enough room upfront if retaining simulated positions matters. Also call viewer.insert_shape for each new body so the objects can be displayed.

Runtime controls​

Use state.set_rbd_gravity for gravity changes, and state.set_multibody_motor_velocity for multibody motor control. These methods update the running simulation without rebuilding its rigid-body buffers.

Resetting an environment​

RbdSnapshot and GPU reset-template APIs are not currently exposed by the Python bindings. Construct a fresh NexusState, insert its objects, then call state.finalize to restart from the authored scene.

An RbdSnapshot doesn't reconstruct collider or joint topology, and isn't a complete multiphysics serialization.

Determinism​

By default, two runs of the same scene can diverge slightly: GPU threads fill the contact and particle lists in a different order on every run. The deterministic mode, off by default, puts these lists in a fixed order so that two runs of the same scene give bit-identical results, at some performance cost:

state.set_deterministic(viewer, True)

It can be enabled before or after state.finalize, and covers both rigid bodies and MPM particles. The two runs must also have the same inputs: timestep, substeps, and insertions and removals at the same steps. The guarantee only holds on the same machine with the same build, unlike Rapier's cross-platform determinism. viewer.sync applies the viewer's own setting to the state it syncs, so enable it on the viewer instead when one is used: viewer.set_deterministic(True).