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 backend.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:
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)
The 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.
info

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.

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.

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:

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,
)?;
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:

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