跳到主要内容

多物理场耦合仿真

在 LambdaSim 中,多物理场耦合仿真指同一个场景里同时运行多个求解器,由它们分别计算不同的物理场,并在仿真步内交换所需的数据。例如刚体(机器人、机械臂、道具)由 MuJoCo 求解器推进,布料、线缆等软体由 IPC 求解器推进;两者可以发生接触,也可以只让一方影响另一方。

本页介绍最常见的一种组合:刚体由 MuJoCo 推进,软体由 IPC 推进,并且软体不反向推动刚体。我们以仓库中的 Go2 布料示例为例,说明怎样把这两个求解器组合起来、它们在每一步交换什么数据,以及怎样换成自己的模型。

阅读前请先按安装说明配置好 LambdaSim 运行环境,并确认能从仓库根目录运行示例脚本。

运行后你会看到: Go2 在重力下趴到地面,布料从上方落下,先接触狗身,再滑到地面;布料会被 Go2 挡住或挂住,但不会推动 Go2。

运行示例​

示例脚本是 examples/go2_ipc_cloth_demo.py,所需资产都在仓库中:

资产用途
tests/assets/Go2/usd/go2.usdGo2 机器人,刚体、关节和碰撞体。
examples/assets/go2_cloth/geometry.usdc布料网格,初始位于 z = 0.50 m。
examples/assets/go2_cloth/constraints.usdc布料的拉伸、剪切约束。
examples/assets/go2_cloth/ground.usdc地面,碰撞和显示在同一个静态 body 上。

从 LambdaSim 仓库根目录运行:

# 打开编辑器,用时间轴播放
QT_QPA_PLATFORM=xcb .venv/bin/python examples/go2_ipc_cloth_demo.py

# 用 CPU 计算
QT_QPA_PLATFORM=xcb .venv/bin/python examples/go2_ipc_cloth_demo.py --device cpu

# 无显示环境:只算 0.8 秒并打印结果
.venv/bin/python examples/go2_ipc_cloth_demo.py --headless --seconds 0.8

GUI 默认使用 cuda:0,时间轴以 60 fps 自动播放。headless 会打印类似:

{'contacts': 136, 'committed': True, 'cloth_positions': (1, 1, 221, 3)}

contacts 是最后一帧布料与 Go2 的接触数,committed=True 表示 IPC 正常提交。示例脚本已包含编辑器所需的显示同步;自己搭场景时,布料需要设成 is_rigid_object=False,并参考示例注册显示回调。

两个求解器如何配合​

求解器负责
MujocoEktroSolver推进 Go2 的刚体,处理关节、重力、地面接触。
SolverVeraIPC推进布料;同时为 Go2 的每个 body 维护一个碰撞代理。

组合的关键只有三点:

  1. 注册顺序:先注册 MuJoCo,再注册 IPC;
  2. 共享位姿:MuJoCo 每帧发布刚体位姿,IPC 读取这些位姿;
  3. drives 连接:IPC 为每个刚体代理写一条 drive,说明它跟随哪个 body。
MujocoEktroSolver ──▶ 刚体位姿 ──▶ SolverVeraIPC
推进 Go2 推进布料
└──────── 布料不反向推动 Go2 ────┘

IPC 不模拟 Go2 的关节,而是在内部为每个 Go2 body 放一个碰撞代理:形状来自该 body 的碰撞体,位姿每帧从 MuJoCo 更新。布料实际碰到的是这些代理,所以能感知 Go2 的形状和运动。反过来,布料对代理的力不会写回 MuJoCo,因此去掉 IPC 后 Go2 的轨迹完全不变。

一次推进的时序:

Scene 收到目标时间请求 → 拆成若干帧(每帧 1/60 s)
每帧按注册顺序调用:
MuJoCo 推进 Go2(内部 4 小步),发布刚体位姿
IPC 读取位姿,更新代理目标,推进布料(内部 2 小步)
所有帧完成后:执行显示同步回调

搭建步骤​

场景与刚体​

import ektro as ek
from lambdasim.rigid_entity import build_rigid_entity
from lambdasim.scene.scene import Scene
from lambdasim.scene.scene_object import VisualizationSetting

scene = Scene()

# 抬高一个脚球半径(0.022 m),避免 Go2 初始姿态穿进地面。
scene.add_usd__ektro(
"tests/assets/Go2/usd/go2.usd",
name="go2",
transform=ek.vec3(0.0, 0.0, 0.022),
)

cloth_object = scene.add_usd__ektro("examples/assets/go2_cloth/geometry.usdc", name="cloth")
# 布料顶点每帧原地更新,必须按 flexible 上传,否则编辑器中会停在初始位置。
cloth_object.is_rigid_object = False

scene.add_usd__ektro("examples/assets/go2_cloth/constraints.usdc", name="curves")
scene.get_object("curves").visualization = VisualizationSetting(visible=False)

ground_object = scene.add_usd__ektro("examples/assets/go2_cloth/ground.usdc", name="ground")
ground_object.visualization = VisualizationSetting(color=(0.72, 0.72, 0.70))

rigid_entity = build_rigid_entity(scene, floating=True)
scene.set_rigid_entity(rigid_entity)

# Go2 所有 body 的 Scene 路径,后面的 IPC 配置会用到。
robot_bodies = tuple(
scene_path
for _, scene_path in rigid_entity.body_canonical_paths
if scene_path is not None and scene_path.startswith("/root/obj/go2/")
)
  • add_usd__ektro(..., name="go2") 会把对象放在 /root/obj/go2/... 下,这个前缀决定后续所有 IPC 路径。
  • build_rigid_entity(scene, floating=True) 表示 Go2 底座可以自由移动;固定底座的机械臂用 floating=False。两个求解器都从这份刚体描述取数据。
  • 约束曲线只参与求解,因此隐藏;is_rigid_object=False 只影响显示,不影响物理。

MuJoCo 求解器​

from lambdasim.solvers.solver_mujoco_ektro import (
MujocoEktroSolver,
MujocoOptions,
SolverSchedule,
)

mujoco = MujocoEktroSolver(
schedule=SolverSchedule(substep_count=4),
mujoco_options=MujocoOptions(),
nconmax=256,
njmax=256,
)
mujoco.src_paths = ["/go2_description", "/Ground"]
scene.add_solver__ektro(mujoco)
  • src_paths 是 USD prim 路径:/go2_description 选中整只 Go2,/Ground 选中地面。不要和 IPC 使用的 /root/obj/... 路径混淆。
  • substep_count=4 把每个 1/60 s 的帧细分成 1/240 s 的小步,接触更稳定。
  • nconmax / njmax 是接触和约束的容量。Go2 趴下时约束峰值约 84,默认估算的 64 会截断约束并产生 NaN;示例显式给 256。
  • 必须先注册 MuJoCo,IPC 才能读到它的位姿。

IPC 求解器与连接​

from lambdasim.solvers import SolverVeraIPC
from solid_dynamics.ipc import IPCSolverConfig

cloth = "/root/obj/cloth/Deformable/SimMesh"
curves = "/root/obj/curves/Constraints/Curves"
ground = "/root/obj/ground/Ground/Body"

affine_domain = {
"kind": "affine",
"motion_type": "dynamic",
"shape_stiffness": 1e4,
"contact_material": "fur",
"collision_group": "robot",
}
affine_drive = {
"group": "rigid",
"body_from_affine": ek.math.identity(ek.mat44),
"translation_strength": 1e4,
"rotation_strength": 1e4,
}

ipc = SolverVeraIPC(
domains={
**dict.fromkeys(robot_bodies, affine_domain),
ground: {
"kind": "affine",
"motion_type": "fixed",
"shape_stiffness": 1e4,
"contact_material": "ground",
"collision_group": "ground",
},
cloth: {
"kind": "triangle_fem",
"contact_material": "fabric",
"collision_group": "soft",
"constraint_path": curves,
"rest_positions_from": "P",
"bending_mode": "off",
},
},
contact={
"default_friction": 0.8,
"collision_policy": "explicit",
"enabled_group_pairs": [["robot", "soft"], ["soft", "ground"]],
},
drives=dict.fromkeys(robot_bodies, affine_drive),
ipc_config=IPCSolverConfig(gravity=(0.0, 0.0, -9.81)),
)
ipc.substep_count = 2
ipc.src_paths = [cloth, ground, *robot_bodies]

# 先加 MuJoCo(3.2 已加),再加 IPC。
scene.add_solver__ektro(ipc)
scene.finalize__ektro(device="cpu") # 有 CUDA 时用 "cuda:0"
  • domains 描述每个物体在 IPC 中的角色:Go2 body 是 motion_type="dynamic" 的仿射代理,地面是 "fixed" 障碍,布料是 triangle_fem。contact_material / collision_group 是自定义名字,供接触配置引用。
  • drives 是耦合的核心:键是动态代理的路径,group="rigid" 表示读取 MuJoCo 发布的位姿,body_from_affine 在代理与 body 重合时用单位阵,两个 strength 是软跟随权重(不是硬约束)。
  • drives 的键集合必须与所有动态仿射域完全一致;不一致时 finalize__ektro() 会直接报错。
  • contact 用显式组对:只开 robot × soft 和 soft × ground。Go2 自身的关节和地面接触由 MuJoCo 负责,IPC 不需要重复计算。
  • 地面只注册一次,MuJoCo 和 IPC 共用同一个静态 body。
  • substep_count=2 让布料在每个 1/60 s 帧内再细分到 1/120 s;num_envs 默认 1,IPC 目前只支持单环境。

如果在脚本里推进时间并检查结果:

from lambdasim.scene.step_context import StepContext

scene.step__ektro(StepContext(step_index=0, time=0.8, dt=1.0 / 60.0))
print(ipc.last_reports[-1].contact.contacts, ipc.last_reports[-1].state_committed)
scene.close()

time 是目标时间而不是增量,Scene 会按 dt 拆成多帧。GUI 中由时间轴自动调用这一步。

4 常用参数与排错​

想调整参数
计算设备--device 或 finalize__ektro(device=...),可选 cpu / cuda:0
刚体稳定性SolverSchedule(substep_count=4)
布料稳定性ipc.substep_count = 2
接触/约束容量nconmax / njmax,Go2 用 256;出现 NaN 时先检查这里
代理跟随松紧translation_strength / rotation_strength,越大越贴合
布料摩擦contact["default_friction"]
哪些组接触contact["enabled_group_pairs"]
现象处理
ValueError 说某个组未发布MuJoCo 必须先于 IPC 注册
ValueError 说 body 路径无效IPC 配置要用 /root/obj/...,不是 USD prim 路径
ValueError 说 drive 集合不匹配每个动态仿射域都要有一条同路径 drive
出现 NaN 或 invalid pose增大 nconmax / njmax
布料穿过 Go2检查 enabled_group_pairs,适当增大两个 strength
布料在编辑器中不动设置 cloth_object.is_rigid_object = False
NotImplementedError 提到 num_envsIPC 只支持单环境,保持 num_envs=1

5 换成自己的场景​

  1. 用 add_usd__ektro(..., name="my_robot") 导入资产,name 会成为 /root/obj/my_robot/... 前缀。
  2. 用 build_rigid_entity(scene, floating=...) 生成刚体描述;自由底座用 True,固定底座用 False。
  3. 打印 rigid_entity.body_canonical_paths(或从编辑器参数面板复制 Path),选出会碰到软体的 body。
  4. 每个选中的 body 配一个 motion_type="dynamic" 仿射域和一条 drive,group="rigid",代理与 body 重合时 body_from_affine 用单位阵。
  5. 地面/固定障碍用 motion_type="fixed";布料/线缆用 triangle_fem / line_fem。
  6. 按需配置接触组对,先注册 MuJoCo 再注册 IPC,finalize__ektro() 后先用 headless 检查接触数和 state_committed。
  7. 要在编辑器中查看时,参考示例脚本末尾的 post-sim 回调,把刚体位姿和布料位置同步到显示数据。

更多参考: