隐式 MPM#

Newton 的隐式物质点法(MPM)求解器用于模拟沙、雪等颗粒材料。Isaac Lab 为其提供 Newton 求解器配置、粒子资产类型,以及与刚体求解器的可选耦合。MPM 支持和刚体–MPM 耦合目前均处于实验阶段。

先从 示例与任务 中运行场景。分辨率、收敛、材质设置及 MPM 的近刚性极限见 MPM 求解器。

示例与任务#

这些示例和演示介绍 MPM 的构建与交互,默认使用 Newton GL 进行交互式查看。启动器默认使用 cuda:0;可通过 --device cuda:N 选择其他 GPU。

颗粒下落
uv run isaaclab example mpm-granular
双向耦合球坑
uv run isaaclab example mpm-two-way-coupling
雪球撞击
uv run isaaclab demo snowball-smash
填充茶壶
uv run isaaclab demo teapot-fill --fluid_render_mode particles

Franka 倒料任务#

IsaacContrib-Franka-Pour 任务从重置数据文件恢复回合,该文件包含相互衔接的 14 阶段重置分布。首次使用时,会从标准 Isaac Lab 资产根目录下载含 20,000 行的标准数据文件,因此训练无需另行准备该文件:

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

仓库中的生成器是复现或自定义该分布的可执行参考。在 L40S 级别的 GPU 上约需两分钟,它会写入一个可显式选择的本地数据文件:

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

任务会自动验证数据中保存的内容摘要。设置 ISAACSIM_ASSET_ROOT 可将标准数据文件的查找位置改为兼容的本地或自托管资产目录树。自定义的可复现实验仍可固定摘要。

源容器使用不参与碰撞的解析填充体积,其高度由 env.source_fill_level 控制,取值范围为 (0, 1] 。默认 0.70 表示填充至约 70% 的杯高;粒子数量随指定填充比例变化,而体素大小和粒子间距保持固定。 env.pour_target_frac 独立控制当前粒子总量中必须到达接收容器的比例。

使用标准任务配置,在 Kit 中运行检查点;无需外部回调或粒子覆盖配置:

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

与刚体耦合#

单独使用时,MPM 求解器只推进粒子。刚体充当碰撞体,其运动由其他机制提供,例如写入运动学位姿;材料不会对这些刚体施加反作用力。

双向交互使用 isaaclab_contrib.coupling 中的实验性耦合器。CouplerProxyCfg 将同一个 Newton 模型划分为若干命名条目:

  • 刚体条目,例如 MJWarp,负责机器人或物体的刚体;

  • MPM 条目负责粒子,必须设置 in_place=True;

  • 代理映射将选定的刚体作为碰撞体提供给 MPM 条目,并将产生的冲量反馈给刚体条目。

mpm-two-way-coupling 示例、snowball-smash 演示和 Franka Pour 任务均使用这一方式。代理耦合、ADMM 耦合、数据交换模式及调优见 耦合求解器 。

数据模型#

配置与 USD#

MPM 材料是一种场景资产。MPMObjectCfg 扩展了可变形对象配置,并使用粒子生成器:

  • MPMGridCfg 在局部坐标系中轴对齐的盒体内填充粒子点阵,分辨率由 voxel_size 和 particles_per_cell 决定。

  • MPMPointsCfg 将粒子放置在明确指定的局部位置,可选配速度、质量和半径。

两种生成器都会写入符合 schema 的 UsdGeom.Points 仿真几何体。网格配置仍先在 Isaac Lab 中生成点阵,再写入各点的位置、宽度、速度和质量。逐粒子的材料参数来自 MPMParticleMaterialCfg ,通过 NewtonMPMMaterialAPI 写入。阻尼参数的单位为 [s],写入值为 damping * young_modulus,单位为 [Pa·s]。critical_fraction 等求解器全局设置位于 MPMSolverCfg 中;其中可用 schema 表达的设置通过 NewtonMPMSceneAPI 写入物理场景 prim。

MPM 对象通过 Newton 的复制机制克隆。网格随机扰动在资产局部坐标系中使用固定种子生成一次,因此 USD 克隆共享相同的局部粒子分布。每个环境需要独立分布时,应使用重置事件或域随机化。

求解器网格与粒子生成器应使用相同的体素尺寸。和其他声明式资产一样,将生成的对象加入 InteractiveSceneCfg :

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",
    ),
)

运行时资产#

MPMObject 实现 Isaac Lab 的可变形对象接口,因此场景重置、更新和状态操作可以像处理其他可变形体一样处理它。每个环境实例的粒子数相同,可通过 particles_per_object 获取。MPMObjectData 通过 particle_pos_w、particle_vel_w 和 particle_state_w 提供仿真世界坐标系中的粒子位置、速度和组合状态,可变形体的 nodal_* 属性是这些属性的别名。root_pos_w 和 root_vel_w 分别是每个实例内所有粒子位置和速度的平均值。位置、速度和状态的写入会立即生效,不支持可变形体运动学目标。

配置参考#

字段及默认值以自动生成的 MPMSolverCfg 参考为准。各字段可分为以下几组:

类别

字段

作用

分辨率

voxel_size,以及生成器中的 particles_per_cell

背景网格和粒子的分辨率,决定精度、活跃网格单元数量和内存占用。

网格存储

grid_type、grid_padding、max_active_cell_count、max_*_node_count 容量限制,以及 separate_worlds

稀疏、稠密或固定网格的分配方式、预留容量,以及是否为每个仿真世界分配独立的局部网格。

流变求解

max_iterations、tolerance、solver 和 warmstart_mode

迭代上限、收敛容差、求解器或热启动流程,以及热启动数据来源。

离散化

transfer_scheme、integration_scheme、velocity_basis、strain_basis 和 collider_basis

粒子与网格之间的数据传递、形函数支撑域,以及各场使用的基函数。

材料与背景

critical_fraction 和 air_drag

求解器全局的屈服面坍缩参数,以及空区域的数值阻力。

碰撞体

collider_velocity_mode、collider_normal_from_sdf_gradient 和 project_outside_colliders

碰撞体速度和法线的计算方式,以及每个子步结束后将粒子投影到碰撞体外部的可选操作。

隐式求解已负责处理碰撞体接触。project_outside_colliders 额外在步进后强制校正仍处于碰撞体内部的粒子。仅在确实需要这种几何校正时使用它,不能用它替代正确的初始状态、碰撞几何体或稳定的时间步长。

CUDA 图捕获#

仅当网格存储满足图捕获的稳定性要求时,MPM 步进才支持 CUDA 图捕获:

  • 使用 "fixed" 网格,且 max_active_cell_count 为正值;或者

  • 使用 "sparse" 网格,max_active_cell_count 必须为正,grid_padding=0,速度基函数为 "Q1" 或 "B2"。应变基函数必须以 "pic" 开头,或为 "P0"、"P1d"、"Q1d"、"Q1" 之一;碰撞体基函数必须以 "pic" 开头,或为 "Q1"、"S2"、"S3" 之一。

稠密网格和未设置容量上限的网格不使用图捕获。

重置#

单独使用 MPM 求解器时,资产自动重置不会清除求解器历史,因为共享同一网格的仿真世界无法分别重置各自的历史。使用耦合器时,自动重置会调用耦合求解器的重置方法,将逐世界的重置掩码传递给包括 MPM 在内的所有条目。Franka Pour 等需要完整重置历史的任务,会使用独立的 MPM 仿真世界(separate_worlds=True),在写入完整状态后调用 reset_solver_state() 。在耦合运行中,这也会重置耦合求解器的历史。

可以先试试 交互式 MPM 演示 :反复进行相同的粒子下落实验,对比参考材料,并调整刚度、内聚力、摩擦和硬化参数。

渲染粒子表面#

MPM 仿真状态始终是一组粒子。表面重建是可选的可视化步骤,不会改变粒子运动、碰撞或材质行为。运行茶壶演示可对比可用模式:

# 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

茶壶演示默认显示重建表面。选择 particles 或 both,可在 Kit、Newton GL 或 Newton RTX 中查看原始 MPM 状态。

Kit、Newton GL 和 Newton RTX 可视化器均支持表面渲染。

这段录制展示的是渲染外观,并非另一种本构模型。

茶壶演示在 sim.reset() 后配置 newton.geometry.ParticleSurface ,并在 sim.render() 前更新网格。启用 CUDA 捕获时,其渲染器会捕获表面提取操作,并在查看器的帧生命周期内暂存动态网格。

重建参数应独立于仿真调节:

  • voxel_size 控制表面细节和内存用量,可以小于 MPM 求解器的体素大小。

  • kernel_radius 控制每个粒子对表面的影响范围。建议从粒子间距的三倍左右开始。

  • max_grid_cells 为 CUDA 图捕获提供固定容量的存储。如果重建区域超出预留网格,请增大该值。

  • 各向异性核能更好地保留薄片和拉长的流体特征,但开销高于各向同性核。

要在自己的场景中渲染表面,请在 sim.reset() 后将配置好的 newton.geometry.ParticleSurface 传给 ParticleSurfaceRenderer。在 sim.render() 前调用 update(),提取并准备当前网格。渲染器会处理 CUDA 图捕获、空表面、非活跃粒子和动态拓扑:

ParticleSurfaceRenderer 的实现
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,
        )

MPM 如何接入 Newton 后端#

将 NewtonCfg 的 solver_cfg 设为 MPMSolverCfg ,会启用 NewtonMPMManager 。这是 Newton 管理器的专用实现,负责构建 Newton 的 SolverImplicitMPM。求解器将材料表示为粒子,并在背景体素网格上隐式求解其流变行为。MPM 管理器会对 Newton 管理器作如下配置:

  • 在同一个 Newton 状态对象中原地更新粒子,而不对状态使用双缓冲。

  • 在隐式求解过程中处理碰撞体接触,不使用 Newton 的碰撞处理管线。

  • 将刚体几何体视为碰撞体。它不是刚体或关节系统的动力学求解器,也不接受外部刚体力。模型构建完成前,运动学刚体的质量会被设为零,凸网格则以三角网格形式传入求解器。

限制#

  • 耦合的 MPM 条目必须原地步进,不支持 project_outside_colliders。