Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
29d2b42
perf(rbd): split high-degree bodies, adapt the color budget and size …
sebcrozet Sep 3, 2026
2c857eb
perf(rbd): refit the LBVH in parallel chunks instead of a single work…
sebcrozet Sep 4, 2026
b7e4d9b
perf(rbd): compute the contact right-hand sides inside the sweeps ins…
sebcrozet Sep 4, 2026
4f2a3fa
perf(rbd): compact contact constraints, recomputing angular Jacobians…
sebcrozet Sep 4, 2026
466c376
fix(rbd): find the previous contacts through the dynamic body's list …
sebcrozet Sep 4, 2026
d3da6d8
perf(rbd): LBVH pairs owned by the smaller leaf, stackless branch-fre…
sebcrozet Sep 4, 2026
d98dd3b
perf(rbd): unroll the cuboid SAT and face clipping loops so their sma…
sebcrozet Sep 5, 2026
c4006ef
perf(rbd): reduce the LBVH domain over several workgroups
sebcrozet Sep 5, 2026
1639cf8
fix(rbd): don't bind the empty LBVH refit counter buffer on WebGPU wi…
sebcrozet Sep 5, 2026
ead5df5
feat(rbd): recycle the contacts of pairs that barely moved since thei…
sebcrozet Sep 5, 2026
d470892
fix web compat
sebcrozet Sep 5, 2026
efb0f7b
feat(examples): add more rbd stress scenes
sebcrozet Sep 10, 2026
d90cd27
feat(viewer): add --headless and --snapshot options to the demos
sebcrozet Sep 10, 2026
67fc474
feat(examples): add the 500k pyramids stress scene
sebcrozet Sep 10, 2026
78988e3
fix(rbd): bound the web BVH refit by the active frontier
sebcrozet Sep 10, 2026
9d3fb24
test(rbd): size the polyline warmstart fixtures within WebGPU's defau…
sebcrozet Sep 10, 2026
3db003b
fix(rbd): pick the lowest free color in the topological coloring
sebcrozet Sep 10, 2026
92a5015
perf(rbd): stop checking a constraint for conflicts at its first one
sebcrozet Sep 10, 2026
12ae668
perf(rbd): start large dense scenes with a larger color budget
sebcrozet Sep 10, 2026
0f427bd
feat(testbed): expose the TGS substeps
sebcrozet Sep 11, 2026
a17c77e
perf(rbd): unroll the contact point loops of the solver
sebcrozet Sep 11, 2026
95d7381
refactor(rbd): make the contact point solver independent of the const…
sebcrozet Sep 11, 2026
0618066
feat(rbd): add contact links, matched with the previous step's constr…
sebcrozet Sep 11, 2026
c0397bf
feat(rbd): add color-ordered contact tiles and their solver kernels
sebcrozet Sep 13, 2026
b586a7f
refactor(rbd): solve the contacts from the contact links and tiles
sebcrozet Sep 17, 2026
3284db6
perf(rbd): solve the biased and unbiased constraints with spetialized…
sebcrozet Sep 17, 2026
bd18425
perf(rbd): bucket the colored constraints with workgroup histograms
sebcrozet Sep 17, 2026
592a367
perf(rbd): split each hub's constraints in its own workgroup
sebcrozet Sep 17, 2026
dd213b0
perf(rbd): size each color's dispatch from the previous step's color …
sebcrozet Sep 18, 2026
2ce0387
perf(rbd): solve the sparse last colors in a single workgroup
sebcrozet Sep 18, 2026
e0dae39
perf(rbd): cache the warmstart of the next substep in the contact tiles
sebcrozet Sep 18, 2026
d36287f
feat(rbd): add Rapier's simplified (twist) friction model
sebcrozet Sep 24, 2026
ea0370d
refactor(rbd): say iteration and dispatch instead of sweep
sebcrozet Sep 24, 2026
0ab4a4a
feat(viewer): add an option to wait for the render before stepping
sebcrozet Sep 24, 2026
0152f84
fix(rbd): pick the solver path from the pair capacity until the first…
sebcrozet Sep 24, 2026
ad1ccd7
refactor(rbd): type the hub counters, color stats and resize readback
sebcrozet Sep 24, 2026
a33928b
chore: CI fixes
sebcrozet Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ Nexus is organized into independent physics modules, each available in 2D and 3D
materials, fluids, etc. Supports one-way coupling with rigid-bodies (= rigid-bodies can push particles but particles
cannot push rigid-bodies).

## Rigid-body friction (3D)

Choose the contact model globally with `RbdSimParams::friction_model`:

```rust ignore
use nexus_rbd3d::dynamics::{FrictionModel, RbdSimParams};

let params = RbdSimParams {
friction_model: FrictionModel::Simplified,
..RbdSimParams::default()
};
```

`Coulomb` (the default) solves sliding friction at each contact point.
`Simplified` follows Rapier's twist-friction model: per-point normal constraints,
one sliding-friction constraint at the manifold center, and one angular constraint
resisting twist about its normal. A single-point contact has no twist constraint.
The models approximate friction differently and can produce different motion.
Multibody contacts always use Coulomb, as in Rapier; 2D is unchanged.

Pass these parameters to `RbdState::from_rapier` (identically for every environment),
or call `NexusState::set_rbd_friction_model` before `finalize`. A live `RbdState`
can switch all environments with `set_friction_model(&backend, model)`.
Switching clears incompatible friction warmstarts and preserves normal warmstarts.

## Prerequisites

### Install `cargo gpu`
Expand Down
33 changes: 30 additions & 3 deletions crates/examples2d/all_examples2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ mod mpm_hourglass2;
mod mpm_sand2;
mod mpm_snowball2;

// Large stress scenes, listed in their own picker group.
mod stress_scenes;

/// Declares the demo registry: a `(name, kind)` list for the picker UI and a
/// name -> `run()` dispatcher. Keeping both in one macro keeps them in sync.
macro_rules! demos {
( $( $name:literal => $kind:ident : $module:ident ),* $(,)? ) => {
fn demo_list() -> Vec<(String, DemoKind)> {
let mut demos: Vec<(String, DemoKind)> =
vec![ $( ($name.to_string(), DemoKind::$kind) ),* ];
demos.extend(stress_scenes::demo_list());
demos.sort_by(|a, b| match (a.0.starts_with('('), b.0.starts_with('(')) {
(true, true) | (false, false) => a.0.cmp(&b.0),
(true, false) => std::cmp::Ordering::Greater,
Expand All @@ -46,7 +50,11 @@ macro_rules! demos {
// migrated to the `NexusState` API); discard whatever it yields
// so every arm has the same `()` type.
$( $name => { let _ = $module::run(viewer, pipeline).await; }, )*
_ => eprintln!("Unknown demo: '{name}'"),
_ => {
if stress_scenes::run(name, viewer, pipeline).await.is_none() {
eprintln!("Unknown demo: '{name}'");
}
}
}
}
};
Expand Down Expand Up @@ -85,6 +93,8 @@ struct CliOptions {
metal: bool,
run: bool,
deterministic: bool,
snapshot: Option<(u32, String)>,
headless: bool,
}

fn parse_command_line() -> CliOptions {
Expand All @@ -98,6 +108,8 @@ fn parse_command_line() -> CliOptions {
metal: false,
run: false,
deterministic: false,
snapshot: None,
headless: false,
};

while let Some(arg) = args.next() {
Expand All @@ -110,6 +122,11 @@ fn parse_command_line() -> CliOptions {
"--metal" => opts.metal = true,
"--run" => opts.run = true,
"--deterministic" => opts.deterministic = true,
"--headless" => opts.headless = true,
"--snapshot" => {
let frame = args.next().and_then(|f| f.parse().ok());
opts.snapshot = frame.zip(args.next());
}
_ => {}
}
}
Expand Down Expand Up @@ -143,8 +160,14 @@ pub async fn main() {
}
}

let mut viewer = NexusViewer::new(demos.clone()).await;
viewer = viewer.with_selected_demo(selected);
let mut viewer = if opts.headless {
NexusViewer::new_headless_with_size(demos.clone(), 1600, 1200).await
} else {
NexusViewer::new(demos.clone()).await
};
viewer = viewer
.with_selected_demo(selected)
.with_wait_for_render(true);
if opts.cpu {
viewer = viewer.with_cpu();
}
Expand All @@ -165,6 +188,10 @@ pub async fn main() {
if opts.deterministic {
viewer = viewer.with_deterministic(true);
}
#[cfg(not(target_arch = "wasm32"))]
if let Some((frame, path)) = opts.snapshot.clone() {
viewer = viewer.with_snapshot(frame, path);
}

// The GPU pipelines are owned here (not by `NexusState`) so they can be
// compiled once up-front and reused across demos. A backend switch drops and
Expand Down
49 changes: 49 additions & 0 deletions crates/examples2d/stress_scenes/box_rain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! A grid of boxes dropped into a walled container.

use super::builder::{Camera, Random, Scene};

/// A `cols` × `rows` grid of boxes (slightly jittered sizes) dropped into a walled container.
fn box_rain(camera: Camera, cols: usize, rows: usize) -> Scene {
let mut s = Scene::new(camera);
let width = cols as f32 * 1.2 + 4.0;
let wall_height = rows as f32 * 1.4 + 10.0;
s.rigid([width + 2.0, 1.0], 0.0, 0.5, [0.0, -0.5, 0.0]);
s.rigid(
[1.0, wall_height],
0.0,
0.5,
[-width / 2.0, wall_height / 2.0, 0.0],
);
s.rigid(
[1.0, wall_height],
0.0,
0.5,
[width / 2.0, wall_height / 2.0, 0.0],
);
let mut rand = Random::new(1);
for y in 0..rows {
for x in 0..cols {
let w = 0.6 + rand.next() * 0.5;
let h = 0.6 + rand.next() * 0.5;
let pos = [
x as f32 * 1.2 - (cols - 1) as f32 * 0.6,
2.0 + y as f32 * 1.4,
rand.next() * 0.5,
];
s.rigid([w, h], 1.0, 0.5, pos);
}
}
s
}

pub fn box_rain_40x25() -> Scene {
box_rain(Camera::new(0.0, 15.0, 12.0), 40, 25)
}

pub fn box_rain_100x50() -> Scene {
box_rain(Camera::new(0.0, 30.0, 5.5), 100, 50)
}

pub fn box_rain_900x100() -> Scene {
box_rain(Camera::new(0.0, 60.0, 0.7), 900, 100)
}
Loading
Loading