Implicit MPM#

Newton’s implicit Material Point Method (MPM) solver models particle materials such as sand, snow, and other granular media. Isaac Lab integrates it as a Newton solver configuration, a particle asset type, and an optional coupling with rigid-body solvers. MPM support and rigid–MPM coupling are experimental.

Start with Examples and tasks for runnable scenes. See MPM Solver for resolution, convergence, material settings, and the nearly rigid MPM limit.

Examples and tasks#

These examples and demos introduce MPM construction and interaction. They default to Newton GL for interactive viewing. The launcher uses cuda:0 by default. Pass --device cuda:N to select a different GPU.

Granular drop
uv run isaaclab example mpm-granular
Two-way sphere pit
uv run isaaclab example mpm-two-way-coupling
Snowball smash
uv run isaaclab demo snowball-smash
Teapot fill
uv run isaaclab demo teapot-fill --fluid_render_mode particles

Franka Pour#

The IsaacContrib-Franka-Pour task restores episodes from a reset artifact containing the connected 14-phase reset distribution. The canonical 20,000-row artifact downloads from the standard Isaac Lab asset root on first use, so training needs no artifact setup:

uv run isaaclab train --rl_library rsl_rl --task IsaacContrib-Franka-Pour \
  --num_envs 2048

The checked-in generator remains the executable reference for reproducing or customizing the distribution. It takes about two minutes on an L40S-class GPU and writes a local artifact that can be selected explicitly:

uv run python scripts/tools/generate_franka_pour_reset_dataset.py
uv run isaaclab train --rl_library rsl_rl --task IsaacContrib-Franka-Pour \
  --num_envs 2048 \
  env.reset_dataset_path=datasets/franka_pour/reset_dataset.pt

The task validates the payload’s stored content digest automatically. Setting ISAACSIM_ASSET_ROOT redirects the canonical artifact to a compatible local or self-hosted asset tree. Digest pinning remains available for custom reproducible experiments.

The source uses a non-colliding analytic fill volume whose height is controlled by env.source_fill_level in (0, 1]. The default 0.70 fills roughly 70% of the cup height; the particle count follows the requested fill level while voxel size and particle spacing stay fixed. env.pour_target_frac independently controls the fraction of that live payload that must reach the receiver.

Play a checkpoint in Kit with the canonical task configuration; no external callback or particle override is required:

uv run --extra isaacsim isaaclab play --rl_library rsl_rl --task IsaacContrib-Franka-Pour \
  --checkpoint /path/to/model.pt --num_envs 1 --visualizer kit

Coupling with rigid bodies#

On its own, the MPM solver advances only particles. Rigid bodies act as colliders whose motion comes from elsewhere, such as kinematic pose writes, and the material does not push back on them.

Two-way interaction uses the experimental coupler in isaaclab_contrib.coupling. A CouplerProxyCfg partitions one Newton model into named entries:

  • a rigid entry, for example MJWarp, that owns the robot or object bodies;

  • an MPM entry that owns the particles and must set in_place=True; and

  • a proxy mapping that exposes selected rigid bodies as colliders in the MPM entry and feeds the resulting impulses back to the rigid entry.

The mpm-two-way-coupling example, the snowball-smash demo, and the Franka Pour task all use this pattern. See Coupled Solvers for proxy and ADMM coupling, exchange modes, and tuning.

Data model#

Configuration and USD#

An MPM material is a scene asset. MPMObjectCfg extends the deformable-object configuration and takes a particle spawner:

  • MPMGridCfg fills an axis-aligned local box with a particle lattice whose resolution follows voxel_size and particles_per_cell.

  • MPMPointsCfg places particles at explicit local positions, with optional velocities, masses, and radii.

Both spawners author schema-valid UsdGeom.Points simulation geometry. Grid configurations still generate the lattice in Isaac Lab before authoring explicit points, widths, velocities, and masses. Per-particle material values come from MPMParticleMaterialCfg and are authored with NewtonMPMMaterialAPI. Damping [s] is authored as damping * young_modulus [Pa·s]. Solver-wide settings, including critical_fraction, live on MPMSolverCfg; the schema-representable ones are authored with NewtonMPMSceneAPI on the physics scene prim.

MPM objects are cloned through Newton replication. Grid jitter is generated once in asset-local coordinates with a fixed seed, so USD clones share the same local particle distribution. Use reset events or domain randomization when each environment needs an independent distribution.

Use the same voxel size for the solver grid and particle generator. Add the generated object to an InteractiveSceneCfg like any other declarative asset:

import isaaclab.sim as sim_utils
from isaaclab_newton.assets import MPMObjectCfg
from isaaclab_newton.physics import MPMSolverCfg, NewtonCfg
from isaaclab_newton.sim.spawners.mpm import MPMGridCfg

voxel_size = 0.02

sim_cfg = sim_utils.SimulationCfg(
    dt=1.0 / 100.0,
    physics=NewtonCfg(
        solver_cfg=MPMSolverCfg(
            voxel_size=voxel_size,
            max_iterations=100,
            tolerance=1.0e-4,
        ),
        num_substeps=2,
    ),
)

media = MPMObjectCfg(
    prim_path="{ENV_REGEX_NS}/Media",
    spawn=MPMGridCfg(
        lower=(-0.1, -0.1, 0.0),
        upper=(0.1, 0.1, 0.2),
        voxel_size=voxel_size,
        particles_per_cell=2.0,
        particle_placement="cell_center",
    ),
)

Runtime asset#

MPMObject implements Isaac Lab’s deformable-object interface, so scene reset, update, and state workflows treat it like other deformables. Each environment instance holds the same number of particles, reported by particles_per_object. MPMObjectData exposes particle positions, velocities, and combined state in the simulation world frame through particle_pos_w, particle_vel_w, and particle_state_w. The deformable nodal_* properties are aliases of these. root_pos_w and root_vel_w are per-instance means over the particles. Position, velocity, and state writers apply immediately. Deformable kinematic targets are not supported.

Configuration reference#

The generated MPMSolverCfg reference is the source of truth for fields and defaults. The fields group as follows:

Group

Fields

Effect

Resolution

voxel_size, and particles_per_cell on the spawner

Background-grid and particle resolution, which set accuracy, active cells, and memory use.

Grid storage

grid_type, grid_padding, max_active_cell_count, the max_*_node_count limits, and separate_worlds

Sparse, dense, or fixed grid allocation, reserved capacity, and whether each world uses its own local grid.

Rheology solve

max_iterations, tolerance, solver, and warmstart_mode

Iteration cap, convergence tolerance, the solver or warm-start sequence, and warm-start source.

Discretization

transfer_scheme, integration_scheme, velocity_basis, strain_basis, and collider_basis

Particle-grid transfer, shape-function support, and the basis functions for each field.

Material and background

critical_fraction and air_drag

Solver-global yield-surface collapse and numerical drag for empty space.

Colliders

collider_velocity_mode, collider_normal_from_sdf_gradient, and project_outside_colliders

Collider velocity and normal computation, and an optional post-substep projection of particles out of colliders.

The implicit solve already resolves collider contact. project_outside_colliders adds a hard post-step correction for particles that remain inside colliders; use it only when that geometric correction is intentional, and not as a substitute for valid initial states, collision geometry, or a stable timestep.

CUDA graph capture#

CUDA graph capture of the MPM step is available only when the grid has capture-stable storage:

  • a "fixed" grid with a positive max_active_cell_count; or

  • a "sparse" grid with a positive max_active_cell_count, grid_padding=0, and a "Q1" or "B2" velocity basis. The strain basis must start with "pic" or be one of "P0", "P1d", "Q1d", or "Q1". The collider basis must start with "pic" or be one of "Q1", "S2", or "S3".

Dense grids and unbounded grids run without capture.

Resets#

With the MPM solver alone, automatic asset resets leave solver history untouched, because worlds that share one grid cannot have their history reset individually. Under a coupler, automatic resets go through the coupled solver’s reset, which passes the per-world reset mask to every entry, including the MPM entry. Tasks that need an exact history reset, such as Franka Pour, use independent MPM worlds (separate_worlds=True) and call reset_solver_state() after writing their complete state. In a coupled run, this also resets the coupled solver’s history.

Try the interactive MPM demo to compare material references and adjust stiffness, cohesion, friction, and hardening on a repeatable particle drop.

Rendering a particle surface#

MPM simulation state remains a set of particles. Surface reconstruction is an optional visualization pass: it does not change particle motion, collisions, or material behavior. Run the teapot demo to compare the available modes:

# Reconstructed surface
uv run isaaclab demo teapot-fill \
  --visualizer newton_gl --fluid_render_mode surface
# Surface and source particles together
uv run isaaclab demo teapot-fill \
  --visualizer newton_gl --fluid_render_mode both
# Path-traced translucent surface
uv run --extra ovrtx isaaclab demo teapot-fill \
  --visualizer newton_rtx --fluid_render_mode surface
# Isaac Sim Kit surface
uv run --extra isaacsim isaaclab demo teapot-fill \
  --visualizer kit --fluid_render_mode surface

The teapot demo defaults to the reconstructed surface. Select particles or both to inspect the source MPM state in Kit, Newton GL, or Newton RTX.

Surface rendering is available in the Kit, Newton GL, and Newton RTX visualizers.

This recording demonstrates rendering appearance, not a different constitutive model.

The teapot demo configures newton.geometry.ParticleSurface after sim.reset() and updates the mesh before sim.render(). Its renderer captures extraction on CUDA when enabled and stages the dynamic mesh inside the viewer’s frame lifecycle.

Tune reconstruction independently from the simulation:

  • voxel_size controls surface detail and memory use. It can be smaller than the MPM solver voxel size.

  • kernel_radius controls how far each particle contributes to the surface. Start near three times the particle spacing.

  • max_grid_cells provides fixed-capacity storage for CUDA graph capture. Increase it if the reconstructed domain outgrows the reserved grid.

  • Anisotropic kernels preserve sheets and stretched fluid features better, but cost more than isotropic kernels.

To render a surface in your own scene, pass the configured newton.geometry.ParticleSurface to ParticleSurfaceRenderer after sim.reset(). Call update() before sim.render() to extract and stage the current mesh. The renderer handles CUDA graph capture, empty surfaces, inactive particles, and dynamic topology:

ParticleSurfaceRenderer implementation
class ParticleSurfaceRenderer:
    """Extract one particle surface and stage its latest mesh for active viewers.

    Configure a Newton ``ParticleSurface`` and construct this renderer after
    resetting the simulation. Call :meth:`update` before rendering each frame
    to reconstruct the surface from the current particle state.
    """

    def __init__(
        self,
        visualizers: Iterable[BaseVisualizer],
        surface: ParticleSurface,
        *,
        path: str,
        color: tuple[float, float, float],
        opacity: float,
        roughness: float = 0.1,
        use_cuda_graph: bool = True,
        backend_manager: NewtonManager | None = None,
    ) -> None:
        """Allocate reusable extraction storage and optionally capture its CUDA graph.

        Args:
            visualizers: Active visualizers; Kit and Newton viewers are supported.
            surface: Configured Newton surface extractor.
            path: Viewer mesh path.
            color: Surface RGB color in [0, 1].
            opacity: Surface opacity in [0, 1].
            roughness: Surface roughness in [0, 1].
            use_cuda_graph: Capture extraction on CUDA when enabled.
            backend_manager: Newton manager whose particle state is reconstructed. Defaults to the active
                simulation's manager (deprecated; pass it explicitly).

        Raises:
            RuntimeError: If no Kit, Newton GL or RTX visualizer is active.
        """
        from isaaclab_visualizers.kit import KitVisualizer

        visualizers = tuple(visualizers)
        self._visualizers = tuple(
            visualizer
            for visualizer in visualizers
            if isinstance(visualizer, (NewtonGLVisualizer, NewtonRTXVisualizer))
        )
        self._kit_mesh = None
        if any(isinstance(visualizer, KitVisualizer) for visualizer in visualizers):
            from pxr import UsdGeom

            from isaaclab.sim import PreviewSurfaceCfg, bind_visual_material, get_current_stage, spawn_preview_surface

            self._kit_mesh = UsdGeom.Mesh.Define(get_current_stage(), path)
            self._kit_mesh.CreateSubdivisionSchemeAttr(UsdGeom.Tokens.none)
            self._kit_mesh.CreateDoubleSidedAttr(True)
            self._kit_mesh.SetNormalsInterpolation(UsdGeom.Tokens.vertex)
            material_path = f"{path}/material"
            spawn_preview_surface(
                material_path,
                PreviewSurfaceCfg(diffuse_color=color, opacity=opacity, roughness=roughness),
            )
            bind_visual_material(path, material_path)
        if not self._visualizers and self._kit_mesh is None:
            raise RuntimeError("Particle surface rendering requires a Kit, Newton GL or RTX visualizer.")

        from isaaclab_newton.physics import NewtonManager

        if backend_manager is None:
            backend_manager = NewtonManager._deprecated_active("ParticleSurfaceRenderer()")
        self._model = backend_manager.get_model()
        self._state = backend_manager.get_state_0()
        self._surface = surface
        self._path = path
        self._color = color
        self._opacity = opacity
        self._roughness = roughness
        self._empty_points = wp.empty(0, dtype=wp.vec3, device=self._model.device)
        self._empty_indices = wp.empty(0, dtype=wp.int32, device=self._model.device)
        self._empty_normals = wp.empty(0, dtype=wp.vec3, device=self._model.device)
        self._surface_mesh = None
        self._surface_graph = None
        if use_cuda_graph and self._model.device.is_cuda:
            self._surface_mesh = self._extract_surface()
            with wp.ScopedCapture(device=self._model.device) as capture:
                self._surface_mesh = self._extract_surface()
            self._surface_graph = capture.graph

    def update(self) -> int:
        """Stage the latest surface mesh and return its triangle count."""
        if self._surface_graph is None:
            self._surface_mesh = self._extract_surface()
        else:
            wp.capture_launch(self._surface_graph)

        vertices, indices, normals = self._surface_mesh.to_arrays()
        hidden = vertices is None or vertices.shape[0] == 0
        triangle_count = 0 if hidden else indices.shape[0] // 3
        if hidden:
            vertices = self._empty_points
            indices = self._empty_indices
            normals = self._empty_normals

        if self._kit_mesh is not None:
            from pxr import UsdGeom, Vt

            points = Vt.Vec3fArray.FromNumpy(vertices.numpy())
            self._kit_mesh.GetPointsAttr().Set(points)
            self._kit_mesh.GetFaceVertexIndicesAttr().Set(Vt.IntArray.FromNumpy(indices.numpy()))
            self._kit_mesh.GetFaceVertexCountsAttr().Set(Vt.IntArray(triangle_count, 3))
            self._kit_mesh.GetNormalsAttr().Set(Vt.Vec3fArray.FromNumpy(normals.numpy()))
            if not hidden:
                self._kit_mesh.GetExtentAttr().Set(UsdGeom.PointBased.ComputeExtent(points))
            self._kit_mesh.GetVisibilityAttr().Set(UsdGeom.Tokens.invisible if hidden else UsdGeom.Tokens.inherited)

        for visualizer in self._visualizers:
            visualizer.log_mesh(
                self._path,
                vertices,
                indices,
                normals=normals,
                hidden=hidden,
                backface_culling=False,
                color=self._color,
                roughness=self._roughness,
                metallic=0.0,
                dynamic=True,
                opacity=self._opacity,
            )
        return triangle_count

    def _extract_surface(self):
        """Extract the surface from the current Newton particle state."""
        return self._surface.extract(
            self._state.particle_q,
            self._model.particle_radius,
            particle_flags=self._model.particle_flags,
            particle_world=self._model.particle_world if self._surface.world_count > 1 else None,
        )

How MPM fits into the Newton backend#

Selecting MPMSolverCfg as the solver_cfg of NewtonCfg installs NewtonMPMManager, a specialization of the Newton manager that constructs Newton’s SolverImplicitMPM. The solver represents material as particles and solves its rheology implicitly on a background voxel grid. The MPM manager configures the Newton manager as follows:

  • It advances particles in place on a single Newton state rather than double buffering state.

  • It resolves collider contact inside the implicit solve. It does not use Newton’s collision pipeline.

  • It treats rigid geometry as colliders. It is not a rigid-body or articulation dynamics solver, and it does not accept external rigid-body forces. Kinematic bodies are given zero mass before the model is finalized, and convex meshes are passed to the solver as triangle meshes.

Limitations#

  • Coupled MPM entries must step in place and do not support project_outside_colliders.