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