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
Callbackend.slow_read_buffer on rbd.body_poses().buffer() to read simulated poses. The following uses
state.rbd2gpu[0] to map the body handle returned by state.insert_rigid_body to its buffer index.
Import khal::backend::Backend to make slow_read_buffer available:
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:
- Example 2D
- Example 3D
let rbd = state.rbd.as_ref().expect("Finalize the state first.");
let mut poses = vec![Pose::IDENTITY; rbd.body_poses().len() as usize];
backend
.slow_read_buffer(rbd.body_poses().buffer(), &mut poses)
.await?;
let gpu_id = state.rbd2gpu[0].get(handle.0).unwrap().gpu_id as usize;
let pose = poses[gpu_id];
println!("Body altitude: {}", pose.translation.y);
let rbd = state.rbd.as_ref().expect("Finalize the state first.");
let mut poses = vec![Pose::IDENTITY; rbd.body_poses().len() as usize];
backend
.slow_read_buffer(rbd.body_poses().buffer(), &mut poses)
.await?;
let gpu_id = state.rbd2gpu[0].get(handle.0).unwrap().gpu_id as usize;
let pose = poses[gpu_id];
println!("Body altitude: {}", pose.translation.y);
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)
gpu_id returned through state.rbd2gpu already accounts for the interleaved environment layout.
Use poses[gpu_id] directly. Retrieve gpu_id again after state.finalize rebuilds the scene, since a topology change
can change the mapping.
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.
backend.slow_read_bufferstate.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::translationRigidBody.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.
state.rbd_world(env) returns read-only access to the authored
PhysicsWorld.
Calling state.rbd_world_mut(env) marks that world for rebuilding. The state.rbd_world_mut_untracked(env) method
doesn't, but changes made through it won't reach the compute buffers on their own. It is intended for advanced code
that explicitly uploads the result through a runtime API.
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_body or state.add_rigid_bodiesstate.add_rigid_bodies can append
bodies directly to the running buffers while the reserved space remains:
- Example 2D
- Example 3D
let mut state = NexusState::default();
state.reserve_rigid_bodies(128);
let ground = RigidBodyBuilder::fixed().build();
let collider = ColliderBuilder::cuboid(5.0, 0.1).build();
state.insert_rigid_body(ground, collider, RbdCoupling::None);
state.finalize(backend).await?;
let body = RigidBodyBuilder::dynamic()
.translation(Vec2::new(0.0, 3.0))
.build();
state.add_rigid_body(
backend,
body,
ColliderBuilder::ball(0.5).build(),
RbdCoupling::None,
)?;
let mut state = NexusState::default();
state.reserve_rigid_bodies(128);
let ground = RigidBodyBuilder::fixed().build();
let collider = ColliderBuilder::cuboid(5.0, 0.1, 5.0).build();
state.insert_rigid_body(ground, collider, RbdCoupling::None);
state.finalize(backend).await?;
let body = RigidBodyBuilder::dynamic()
.translation(Vec3::new(0.0, 3.0, 0.0))
.build();
state.add_rigid_body(
backend,
body,
ColliderBuilder::ball(0.5).build(),
RbdCoupling::None,
)?;
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_body and state.add_rigid_bodiesstate.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.control_multibody_motorsstate.set_multibody_motor_velocity for
multibody motor control. These methods update the running simulation without rebuilding
its rigid-body buffers. Call state.set_rbd_sim_params before the simulation starts, because it marks the scene for rebuilding.
Resetting an environment
In 3D, RbdState::snapshot reads poses, velocities, and multibody state from environment 0 into an RbdSnapshot.
Call RbdState::reset_env_from_snapshot to restore an environment with the same topology:
let rbd = state.rbd.as_mut().expect("Finalize the state first.");
let initial_state = rbd.snapshot(backend).await;
// Later, restore environment 0 to this state (with the same topology).
rbd.reset_env_from_snapshot(backend, 0, &initial_state);
Take the initial snapshot once and retain it for later resets. For large batches, RbdState::publish_reset_templates and
RbdState::reset_envs_from_templates keep templates on the GPU and apply several resets together.
The template and destination layouts must match.
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:
- Example 2D
- Example 3D
state.set_deterministic(backend, true);
state.set_deterministic(backend, true);
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: NexusViewer::with_deterministic(true)viewer.set_deterministic(True).