场景中有两路相互独立的液体:

  • 左侧悬空管道在世界坐标系中持续释放浅咖啡色的成簇连续流;
  • 三连杆铰接臂带动水壶倾倒,绑定在 kettle_link 壶口上的出口喷洒近透明水。

两路液体都是真实 Isaac Lab 粒子流体,录像来自场景内 1280×720 RGB 相机。

这个案例包含什么

能力 对应位置
固定容量粒子池 run.yaml 中的 scenario.scene.fluids
每种液体独立的物理与外观材质 fluid_18000.yamlfluid_8000.yaml
世界坐标固定出口 main.py 中的 pipe-coffee
绑定铰接 Link 的出口 kettle-spoutFluidAttachment
有限储液量 FluidReservoirSpec
倾斜触发出液 FluidTrigger.gravity_pour
成簇连续流 spray_half_angle_rad=0.0
锥形喷洒 spray_half_angle_rad=math.radians(40.0)
实时粒子状态查询 simulation.particle_fluid(...)
不依赖插件的纯配置储液与出口 declarative.yaml

场景配置

run.yaml 负责搭建环境、铰接水壶机械臂、两组液体粒子池和 RGB 相机。两组粒子池分开声明, 因为它们的容量和外观不同:

  • fluids.pipe_coffee:浅咖啡色,18,000 个粒子;
  • fluids.kettle_water:近透明水,8,000 个粒子。

color_rgba 是每个液体 component 自己的属性。修改水壶里的水不会改变管道咖啡的颜色, 也不会改变密度、黏度和表面张力等物理属性。

declarative.yaml 展示更短的纯配置路径。reservoirsemitters 是每个 fluid 实体下可选的 Core 字段;Runtime 在 Run prepare 时创建它们,并在 reset 时恢复。 整个过程不需要安装液体插件,也不需要 Python 初始化代码。

成簇连续流与喷洒流

管道中所有粒子的速度方向沿同一根轴,因此形成连续的成簇流:

python
FluidEmitterRequest(
    emitter_id="pipe-coffee",                       # 发射器的唯一 ID。
    fluid_id="fluids.pipe_coffee",                 # 使用哪一个粒子池。
    position_m=(0.0, 0.0, 0.52),                   # 世界坐标系中的出口位置。
    direction=(0.0, 0.0, -1.0),                    # 竖直向下。
    rate_particles_s=2000.0,                       # 每秒释放的粒子数。
    particle_count=18_000,                         # 最多释放多少粒子。
    spray_half_angle_rad=0.0,                      # 零角度表示不喷洒。
)

水壶出口跟随铰接物体的 kettle_link。每个粒子的速度方向会确定性地分布在 40° 半角锥体内:

python
FluidEmitterRequest(
    emitter_id="kettle-spout",                     # 壶口发射器 ID。
    fluid_id="fluids.kettle_water",                # 近透明水粒子池。
    attachment=FluidAttachment(
        parent="robots.pour_arm",                  # 绑定到哪个场景实体。
        link_name="kettle_link",                   # 跟随水壶 Link,而不是机器人根节点。
        position_m=(0.405, 0.0, 0.085),             # 壶口相对 Link 的位置。
        direction_local=(1.0, 0.0, 0.0),            # 壶口局部出水方向。
    ),
    reservoir_id="kettle-reservoir",               # 从有限储液容器扣减水量。
    flow_rate_ml_s=22.0,                            # 每秒流量,单位 ml。
    spray_half_angle_rad=math.radians(40.0),        # 40° 半角锥形喷洒。
    trigger=FluidTrigger.gravity_pour(
        start_angle_deg=28.0,                       # 到达该倾角后开始出液。
        stop_angle_deg=38.0,                        # 回正时使用滞回阈值避免抖动。
    ),
)

spread_radius_m 表示出口本身的半径,单纯增大它不会变成喷洒。 spray_half_angle_rad 控制速度方向的锥形扩散;设为零就是成簇连续流。

喷洒使用确定性随机数。同一份 Run 和相同 seed 会生成相同的初始粒子方向,方便复现。

Reservoir 与触发逻辑

水壶的 reservoir 容量是 150 ml,初始有 120 ml。发射器启动后先处于 armed 状态。 机械臂驱动 pour_joint 倾倒水壶,壶口方向达到重力倾倒阈值后才开始喷洒;剩余水量不足以 生成下一个粒子时自动结束。

运行

bash
cd demo/fundamentals/17_liquid
fastsim config validate run.yaml --project ../.fastsim/project.yaml --offline
fastsim config validate declarative.yaml --project ../.fastsim/project.yaml --offline
# 不安装液体插件,直接运行纯配置版本:
fastsim run declarative.yaml --project ../.fastsim/project.yaml
python main.py

程序会打开可见的 Isaac Lab 窗口,并生成:

text
output/17-liquid-stream-and-spray-1280x720.mp4

MP4 来自仿真场景内相机,不是桌面或窗口录屏。

Record → Replay

FastSim Core 可以通过按需启用的 fluid.audit 服务发布每个已经被后端接受的非空发射 Batch。Record 0.3.8 把这些紧凑的确定性输入写入 FSR 的 fluid.emissions Stream, 不会在每一帧复制全部活动粒子。Replay 0.2.15 在 Prepare 阶段通过 fluid.replay 加载完整 Stream,关闭配置中的实时发射器,并在记录的 Tick、Physics 之前注入重建后的 Batch;之后的粒子运动仍由物理仿真继续计算。

本案例的离线 Project 目前只包含共享 Component Catalog,并显式设置 installed_packages: false。因此,在 Project Catalog 能解析这两个精确插件版本之前, 这里不会提交无法运行的 Record/Replay Run 文件。

与规划几何的关系

粒子流体永远不会进入 planning.scene。规划器能够看到管道、水壶、机械臂和地面的碰撞几何, 但不会看到成千上万个短生命周期液体粒子。需要液体状态时,应通过专门的 particle-fluid 查询接口读取。

Backend 说明

这个案例需要原生粒子流体能力,目前已经在 Isaac Lab adapter 上验收。其他 backend 必须先 声明并实现相同的液体 capability,才能被这份 Run 选择。

验证等级

真实可见 Isaac 已通过。 合并后的案例实际释放管道粒子 17,600 个、水壶粒子 7,600 个; 水壶发射器正常完成,逻辑 reservoir 剩余 1.25 ml。场景相机生成 1280×720、24 FPS、310 帧 MP4。