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.
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():
- Example 2D
- Example 3D
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?;
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:
viewer.render_framehandles the window and its controls.- If
viewer.simulating()returnstrueTrue,pipeline.simulateadvances the simulation. viewer.syncupdates 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.