Skip to main content

Simulation state and viewer

The NexusState, NexusPipeline, and NexusViewer are the three structures used by the basic simulation example. The NexusState contains the scene, NexusPipeline advances the simulation, and NexusViewer displays its result. Keep these structures alive between frames, so their buffers and compute pipelines can be reused.

Simulation state​

The NexusState stores the scene description and the compute buffers used by the simulation. For rigid-body scenes, RigidBody, Collider, and joint builders describe the initial objects on the CPU. Calling state.finalize converts this description into compute buffers. For MPM, state.add_particles uploads the Particle values. The physics modules are initialized lazily, so a NexusState without particles doesn't need to allocate an MPM grid.

warning

The RigidBody values used to construct the scene are not a live copy of the running simulation. Changing or reading them after pipeline.simulate doesn't automatically change or read the simulated bodies. Use the APIs described in reading and modifying rigid-bodies.

Finalization and stepping​

Call viewer.init_backend before state.finalize. The pipeline.simulate method then finalizes pending changes and runs the solvers. The first call is more expensive because the compute pipelines are created lazily. It is possible to pay this cost upfront with pipeline.preload_pipelines. The NexusPipelineMask argument selects the physics modules to preload. pipeline.preload_pipelines(viewer) preloads both physics modules.

The following continues the rigid-body scene from the getting started example, using an initialized GpuBackenda viewer initialized with viewer.init_backend():

let mut pipeline = NexusPipeline::default();
pipeline.preload_pipelines(backend, NexusPipelineMask::RBD)?;
state.finalize(backend).await?;
state.set_rbd_gravity(backend, [0.0, -9.81, 0.0]);
pipeline.simulate(backend, state, None).await?;
pipeline = NexusPipeline()
state.finalize(viewer)
state.set_rbd_gravity(viewer, Vec3(0.0, -9.81, 0.0))
pipeline.simulate(viewer, state)

The state.set_rbd_gravity call comes after state.finalize, because it updates the allocated rigid-body buffers. See rigid-body integration parameters for the rigid-body timestep, and MPM parameters for SimulationParams and state.set_mpm_substeps.

NexusPipeline::simulate takes a &GpuBackend, so it can also be used without NexusViewer. The await expressions let initialization, uploads, and readbacks complete when required. Methods that need a backend receive the viewer. These Python calls wait for the corresponding asynchronous Rust operations internally.

Registering shapes​

Calling state.insert_rigid_body doesn't register graphics. Call viewer.insert_shape with the returned body handle, the collider.shared_shape(), and its local Pose, as in the getting started example. Clone collider.shared_shape() before moving the collider into state.insert_rigid_body.

The local_pose argument of viewer.insert_shape is relative to the rigid-body, like a collider's position. For a centered shape, pass Pose::IDENTITYPose.IDENTITY. Passing the body's world-space Pose would apply its translation twice. Use viewer.insert_shape_in with an environment index for a batch. Several calls to viewer.insert_shape can register several shapes for the same body.

The simulation loop​

The loop in the getting started example uses three operations:

  1. viewer.render_frame handles the window and its controls.
  2. If viewer.simulating() returns trueTrue, pipeline.simulate advances the simulation.
  3. viewer.sync updates the graphics from the simulation state.

Call viewer.sync even if viewer.simulating() returns falseFalse, so the graphics reflect initialization and edits. The viewer.with_running constructor option starts the simulation immediately. viewer.render_frame and viewer.sync are asynchronous. Run NexusViewer on the main thread. Its methods block until the corresponding Rust operation completes.

In 3D, viewer.scene3d_mut().add_directional_lightviewer.add_directional_light adds lighting. viewer.set_camera(eye, target) points the camera towards the objects. For the complete render loop, start from the compiling example.

Backend selection​

The default NexusViewer backend uses WebGPU. Select a different backend before calling viewer.init_backend. The viewer crates provide the metal, cuda, cpu, and cpu-parallel Cargo features. Use viewer.with_cpu() for CPU physics, or viewer.with_backend(BackendType::Metal) and viewer.with_backend(BackendType::Cuda) for the corresponding native GPU backend after enabling its feature.

viewer.with_backend(name) selects a backend by name: "webgpu" (the default), "cpu", and "metal" or "cuda" for builds compiled with that feature. nexus3d.available_backends() lists the names supported by the installed build, and viewer.backend_name() returns the selected one. The macOS wheels include Metal support. When building the bindings yourself, enable the backend's feature, for example:

maturin develop --release -m crates/nexus_python3d/Cargo.toml --features metal

The CPU backend executes physics kernels on the CPU. A NexusViewer still needs a graphics device for rendering. NexusViewer::new_headless_with_size creates an offscreen viewer. NexusViewer(width=640, height=480, headless=True) creates an offscreen viewer, and viewer.snap_rgb() returns an RGB image as a NumPy array. An offscreen NexusViewer avoids opening an OS window, but still needs a working rendering backend.

Checking the simulation loop​

If the simulation doesn't start, check that viewer.init_backend was called, that the scene contains objects, and that viewer.simulating() allows the loop to reach pipeline.simulate. Call state.finalize before viewer.sync when displaying the initial state without advancing it.

Use cargo run --releasematurin develop --release for a source build when evaluating performance. Measure several calls to pipeline.simulate after initialization, and reuse the same pipeline. Calls to viewer.sync, viewer.render_frame, and CPU readbacks contribute to frame time too. A tiny scene may not contain enough work to compensate for compute dispatch overhead.

The repository's demo browser can be started with:

cargo run --release --bin all_examples3
cargo run --release --bin all_examples2

The online demos use these applications with the wasm32-unknown-unknown target. They need WebGPU for compute and rendering.