Particles and materials
Nexus provides the Material Point Method (MPM) for materials that deform, like elastic objects, sand, snow, or fluids.
The Particle values carry the material state, and an auxiliary grid computes their interaction.
The ParticleModel determines the material's response to deformation.
Unlike rigid-bodies with ball colliders, MPM particles interact through the grid. A particle's radius determines its initial volume and visualization size.
Creation and insertion
Import Particle, ParticleModel, and SimulationParams from nexus2d::mpm::solver or nexus3d::mpm::solver.
Construct Particle::new(position, radius, density, model),
then call state.add_particles to upload a group of particles. Its return value identifies a chunk.
The following adds a block of elastic material to an existing simulation, using an initialized backend:
- Example 2D
- Example 3D
let cell_width = 0.5;
let params = SimulationParams {
gravity: Vec2::new(0.0, -9.81),
dt: 1.0 / 60.0,
padding: 0.0,
};
state.set_mpm_params(backend, params, cell_width)?;
state.set_mpm_substeps(20);
let model = ParticleModel::elastic(1.0e4, 0.2);
let mut particles = Vec::new();
for x in 0..4 {
for y in 0..4 {
let position = Vec2::new(
x as f32 * cell_width / 2.0,
2.0 + y as f32 * cell_width / 2.0,
);
particles.push(Particle::new(position, cell_width / 4.0, 1000.0, model));
}
}
let chunk = state.add_particles(backend, particles)?;
let cell_width = 0.5;
let params = SimulationParams {
gravity: Vec3::new(0.0, -9.81, 0.0),
dt: 1.0 / 60.0,
};
state.set_mpm_params(backend, params, cell_width)?;
state.set_mpm_substeps(20);
let model = ParticleModel::elastic(1.0e4, 0.2);
let mut particles = Vec::new();
for x in 0..4 {
for y in 0..4 {
for z in 0..4 {
let position = Vec3::new(
x as f32 * cell_width / 2.0,
2.0 + y as f32 * cell_width / 2.0,
z as f32 * cell_width / 2.0,
);
particles.push(Particle::new(position, cell_width / 4.0, 1000.0, model));
}
}
}
let chunk = state.add_particles(backend, particles)?;
Here, cell_width is the grid cell width, the particle spacing is cell_width / 2, and the particle radius is cell_width / 4.
This gives two particles per cell along each axis, which is a useful starting point for regular blocks.
The initial volume is the particle diameter raised to the dimension, and mass is this volume multiplied by density.
Call state.set_mpm_params before state.add_particles. Calling state.set_mpm_params again after upload resets
the grid, so it is preferable to choose the cell width at setup. pipeline.simulate advances the uploaded material,
and viewer.sync updates its graphics. The simulation loop is the
same as for the getting started example.
Material models
The young_modulus argument controls stiffness. Increasing it makes deformation more difficult, but also requires
smaller substeps for stability. The poisson_ratio argument controls lateral deformation during compression or
stretching. The following shows ParticleModel::elastic and ParticleModel::sand:
- Example 2D
- Example 3D
let elastic = ParticleModel::elastic(1.0e4, 0.2);
let sand = ParticleModel::sand(1.0e5, 0.2);
let elastic = ParticleModel::elastic(1.0e4, 0.2);
let sand = ParticleModel::sand(1.0e5, 0.2);
An elastic material tends to recover its initial shape. Sand also has a plastic response, so a pile can
retain a new shape. ParticleModel::elastic_neo_hookean and ParticleModel::sand_neo_hookean
use Neo-Hookean elasticity.
The Rust API also provides ParticleModel::snow, ParticleModel::cohesive_sand, and
ParticleModel::water_for_depth(density, gravity, depth, max_compression). The latter chooses a bulk modulus from
the expected water depth and tolerated max_compression. The
MPM examples contain complete scenes using these models.
Initial state
Before state.add_particles, set particle.dynamics.velocity for an
initial velocity. Use particle.dynamics.set_damping and particle.dynamics.set_fixed to configure damping or hold particles in place. This is typically used to attach
part of an elastic object to a fixed support.
Keep in mind that modifying the original particle after state.add_particles doesn't modify its uploaded copy.
For an emitter, create new Particle values and call state.extend_chunk as shown below.
Timestep and gravity
The params.dt value in SimulationParams is the total MPM time advanced by one pipeline.simulate call.
The state.set_mpm_substeps method divides this time into substeps; its default is 20, and counts smaller than
one are clamped to one. With params.dt = 1 / 60 and state.set_mpm_substeps(20), each substep advances 1 / 1200
seconds. Increasing state.set_mpm_substeps improves stability at the expense of computations.
The params.gravity value defines initial MPM gravity. After state.set_mpm_params, call state.set_mpm_gravity
to change it without resetting the grid. It is separate from state.set_rbd_gravity; setting one doesn't change the
other. See coupling when both solvers interact.
Reducing cell_width or increasing young_modulus can require shorter substeps. If particles gain large velocities
or disappear, increase state.set_mpm_substeps, reduce params.dt, or reduce the ParticleModel constructor's
young_modulus. Increasing the number of particles alone doesn't fix an unstable timestep.
Also check that radius and density passed to Particle::new are positive,
and that the initial particle positions don't lie inside an unintended boundary.
For large materials, NexusCapacities::mpm_grid_size and NexusCapacities::mpm_particles configure the initial
sparse-grid and particle capacities passed to NexusState::new.
Chunks and emitters
The handle returned by state.add_particles identifies the uploaded group. Call state.extend_chunk to append
particles, and state.remove_chunk to remove the group. Given chunk from the creation example:
- Example 2D
- Example 3D
let model = ParticleModel::sand(1.0e5, 0.2);
let particle = Particle::new(Vec2::new(0.0, 3.0), 0.125, 1000.0, model);
state.extend_chunk(backend, chunk, vec![particle])?;
state.remove_chunk(backend, chunk)?;
let model = ParticleModel::sand(1.0e5, 0.2);
let particle = Particle::new(Vec3::new(0.0, 3.0, 0.0), 0.125, 1000.0, model);
state.extend_chunk(backend, chunk, vec![particle])?;
state.remove_chunk(backend, chunk)?;
Removing particles can move other particles inside the compute buffers. Use the chunk handle for chunk operations;
a raw particle buffer index is not a persistent identifier. Don't reuse the handle after state.remove_chunk.
Boundaries
The RbdCoupling::None value leaves particles unaffected by a
rigid-body collider. To hold the material above a ground, supply
RbdCoupling::MpmOneWay to state.insert_rigid_body.
The MPM coupling page shows the full setup and BoundaryCondition choices.