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.

pipeline.preload_pipelines(viewer) preloads both physics modules.

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

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.

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.

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.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 True, pipeline.simulate advances the simulation.
  3. viewer.sync updates the graphics from the simulation state.

Call viewer.sync even if viewer.simulating() returns False, so the graphics reflect initialization and edits. The viewer.with_running constructor option starts the simulation immediately.

Run NexusViewer on the main thread. Its methods block until the corresponding Rust operation completes.

In 3D, viewer.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.

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(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 maturin 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.