Skip to main content

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. Import Particle, ParticleModel, and SimulationParams from nexus3d. Construct Particle::new(position, radius, density, model)Particle(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 backenda viewer initialized with viewer.init_backend():

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)?;
cell_width = 0.5
params = SimulationParams(Vec3(0.0, -9.81, 0.0), 1.0 / 60.0)
state.set_mpm_params(viewer, params, cell_width)
state.set_mpm_substeps(20)
model = ParticleModel.elastic(1.0e4, 0.2)
particles = []
for x in range(4):
for y in range(4):
for z in range(4):
position = Vec3(x * cell_width / 2.0,
2.0 + y * cell_width / 2.0,
z * cell_width / 2.0)
particles.append(Particle(position, cell_width / 4.0, 1000.0, model))
chunk = state.add_particles(viewer, 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::sandParticleModel.elastic and ParticleModel.sand:

let elastic = ParticleModel::elastic(1.0e4, 0.2);
let sand = ParticleModel::sand(1.0e5, 0.2);
elastic = ParticleModel.elastic(1.0e4, 0.2)
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 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. The Python ParticleModel exposes the elastic and sand constructors above. The Rust ParticleModel::snow, ParticleModel::cohesive_sand, and ParticleModel::water_for_depth constructors are not exposed by these bindings.

Initial state​

Before state.add_particles, set particle.dynamics.velocityparticle.velocity for an initial velocity. Use particle.dynamics.set_damping and particle.dynamics.set_fixedparticle.set_damping and particle.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.

warning

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::newParticle 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:

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)?;
model = ParticleModel.sand(1.0e5, 0.2)
particle = Particle(Vec3(0.0, 3.0, 0.0), 0.125, 1000.0, model)
state.extend_chunk(viewer, chunk, [particle])
state.remove_chunk(viewer, 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::NoneRbdCoupling.NONE value leaves particles unaffected by a rigid-body collider. To hold the material above a ground, supply RbdCoupling::MpmOneWayRbdCoupling.mpm_one_way to state.insert_rigid_body. The MPM coupling page shows the full setup and BoundaryCondition choices.