Getting started
Nexus is a physics engine for simulating rigid-bodies and deformable materials with compute shaders.
It is available in 2D and 3D, through the nexus2d and nexus3d crates3D, through the
nexus3d module. This page shows how to set up a simple simulation with a ball falling on a fixed ground.
The guide describes the APIs in the 0.7 source tree; an older installed release may expose different methods.
The following pages separate rigid-body dynamics from
MPM particles and materials, with simulation state and viewer
covering the common workflow.
Setting up Nexus with Cargo
Nexus relies on the official Rust package manager Cargo for dependency resolution and compilation.
The rbd feature enables rigid-body dynamics, and the mpm feature enables the Material Point Method.
Keep in mind that these two features are not enabled by default.
Nexus uses Rust-GPU to compile its compute shaders. Before building your project, install the shader compiler and the toolchain it needs:
cargo install cargo-gpu --version 0.10.0
cargo gpu install --auto-install-rust-toolchain
Then create a new project and add the dependencies corresponding to the dimension you want to simulate.
NexusViewer is optional for Nexus itself, but is used by the example below.
- Example 2D
- Example 3D
cargo new example-using-nexus
cd example-using-nexus
cargo add nexus2d@0.7 --features rbd,mpm
cargo add nexus_viewer2d@0.7 pollster@1 anyhow@1
cargo new example-using-nexus
cd example-using-nexus
cargo add nexus3d@0.7 --features rbd,mpm
cargo add nexus_viewer3d@0.7 pollster@1 anyhow@1
The nexus2d::rapier and nexus3d::rapier modules re-export the version of Rapier used by Nexus.
Using this re-export avoids mixing different versions of
RigidBodyBuilder and
ColliderBuilder. Their concepts and
builder settings are documented in the Rapier user-guide. Nexus uses its own NexusPipeline, so not every Rapier
setting is implemented by Nexus.
Installing Nexus with pip
The Python package is named dimforge-nexus3d, whereas the module imported by your code is named nexus3d:
python -m pip install dimforge-nexus3d
The plain nexus3d package on PyPI is an unrelated project. Make sure to install dimforge-nexus3d.
The bindings target CPython 3.9 or newer. If a wheel is not available for your platform, build the bindings from
the Nexus repository after installing Cargo and the shader compiler:
cargo install cargo-gpu --version 0.10.0
cargo gpu install --auto-install-rust-toolchain
python -m venv .venv
source .venv/bin/activate
python -m pip install maturin
maturin develop --release -m crates/nexus_python3d/Cargo.toml
The activation command above is for a Unix shell. On Windows, activate .venv with the script in its Scripts directory.
Only the 3D engine is exposed to Python. The Python bindings do not expose every low-level Rust API. This guide indicates these differences where they matter.
Basic simulation example
Here is a complete src/main.rsmain.py file. The
RigidBodyBuilder and
ColliderBuilder describe the scene. The NexusState contains that scene, and the NexusPipeline advances it.
- Example 2D
- Example 3D
use nexus_viewer2d::NexusViewer;
use nexus2d::prelude::{NexusPipeline, NexusState, RbdCoupling};
use nexus2d::rapier::prelude::*;
fn main() -> anyhow::Result<()> {
pollster::block_on(async {
let mut viewer = NexusViewer::new(Vec::new()).await.with_running();
viewer.init_backend();
let mut state = NexusState::default();
let mut pipeline = NexusPipeline::default();
/* Create the ground. */
let ground = RigidBodyBuilder::fixed().build();
let collider = ColliderBuilder::cuboid(5.0, 0.1).build();
let shape = collider.shared_shape().clone();
let handle = state.insert_rigid_body(ground, collider, RbdCoupling::None);
viewer.insert_shape(handle, &shape, Pose::IDENTITY);
/* Create the falling ball. */
let ball = RigidBodyBuilder::dynamic()
.translation(Vec2::new(0.0, 3.0))
.build();
let collider = ColliderBuilder::ball(0.5).build();
let shape = collider.shared_shape().clone();
let handle = state.insert_rigid_body(ball, collider, RbdCoupling::None);
viewer.insert_shape(handle, &shape, Pose::IDENTITY);
/* Upload the scene before rendering it. */
state.finalize(viewer.backend()).await?;
while viewer.render_frame().await {
if viewer.simulating() {
pipeline
.simulate(viewer.backend(), &mut state, None)
.await?;
}
viewer.sync(&mut state, None).await?;
}
Ok(())
})
}
use nexus_viewer3d::NexusViewer;
use nexus3d::prelude::{NexusPipeline, NexusState, RbdCoupling};
use nexus3d::rapier::prelude::*;
fn main() -> anyhow::Result<()> {
pollster::block_on(async {
let mut viewer = NexusViewer::new(Vec::new()).await.with_running();
viewer.init_backend();
let mut state = NexusState::default();
let mut pipeline = NexusPipeline::default();
/* Create the ground. */
let ground = RigidBodyBuilder::fixed().build();
let collider = ColliderBuilder::cuboid(5.0, 0.1, 5.0).build();
let shape = collider.shared_shape().clone();
let handle = state.insert_rigid_body(ground, collider, RbdCoupling::None);
viewer.insert_shape(handle, &shape, Pose::IDENTITY);
/* Create the falling ball. */
let ball = RigidBodyBuilder::dynamic()
.translation(Vec3::new(0.0, 3.0, 0.0))
.build();
let collider = ColliderBuilder::ball(0.5).build();
let shape = collider.shared_shape().clone();
let handle = state.insert_rigid_body(ball, collider, RbdCoupling::None);
viewer.insert_shape(handle, &shape, Pose::IDENTITY);
/* Upload the scene before rendering it. */
state.finalize(viewer.backend()).await?;
viewer
.scene3d_mut()
.add_directional_light(Vec3::new(1.0, -2.0, 3.0));
while viewer.render_frame().await {
if viewer.simulating() {
pipeline
.simulate(viewer.backend(), &mut state, None)
.await?;
}
viewer.sync(&mut state, None).await?;
}
Ok(())
})
}
from nexus3d import (
NexusViewer, NexusPipeline, NexusState, RbdCoupling,
RigidBodyBuilder, ColliderBuilder, Vec3, Pose,
)
def main():
viewer = NexusViewer().with_running()
viewer.init_backend()
state = NexusState()
pipeline = NexusPipeline()
# Create the ground.
ground = RigidBodyBuilder.fixed().build()
collider = ColliderBuilder.cuboid(5.0, 0.1, 5.0).build()
handle = state.insert_rigid_body(ground, collider, RbdCoupling.NONE)
viewer.insert_shape(handle, collider.shared_shape(), Pose.IDENTITY)
# Create the falling ball.
ball = RigidBodyBuilder.dynamic().translation(Vec3(0.0, 3.0, 0.0)).build()
collider = ColliderBuilder.ball(0.5).build()
handle = state.insert_rigid_body(ball, collider, RbdCoupling.NONE)
viewer.insert_shape(handle, collider.shared_shape(), Pose.IDENTITY)
state.finalize(viewer)
viewer.add_directional_light(Vec3(1.0, -2.0, 3.0))
while viewer.render_frame():
if viewer.simulating():
pipeline.simulate(viewer, state)
viewer.sync(state)
if __name__ == "__main__":
main()
Run the example with cargo run --releasepython main.py.
The NexusViewer opens a window, and viewer.with_running() starts the simulation immediately.
The window's controls change the value returned by viewer.simulating(), which determines whether the loop calls pipeline.simulate.
Call viewer.init_backend() before state.finalize or pipeline.simulate. The three operations initialize the backend,
upload the scene, and advance physics respectively. See simulation state and viewer.
pollster::block_on to drive asynchronous work in a native executable. A browser application
needs an asynchronous browser entry point instead. The repository's WebAssembly demos show this setup.
Run NexusViewer on the main thread. A normal Python script is the recommended starting point.