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.
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:
viewer.render_framehandles the window and its controls.- If
viewer.simulating()returnsTrue,pipeline.simulateadvances the simulation. viewer.syncupdates 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.