The scene contains two independent liquids:

  • a suspended pipe emits a coherent, light-coffee stream in the world frame;
  • a three-link articulated arm tilts a kettle, then a link-bound outlet sprays near-transparent water from the spout.

Both are real Isaac Lab particle fluids recorded by the 1280×720 scene camera.

What this case covers

Capability Where to look
Fixed-capacity particle pools scenario.scene.fluids in run.yaml
Per-fluid physical and visual material fluid_18000.yaml and fluid_8000.yaml
World-frame outlet pipe-coffee in main.py
Articulation-link outlet kettle-spout and FluidAttachment
Finite liquid volume FluidReservoirSpec
Tilt-triggered pouring FluidTrigger.gravity_pour
Coherent stream spray_half_angle_rad=0.0
Cone spray spray_half_angle_rad=math.radians(40.0)
Live particle-state query simulation.particle_fluid(...)
Plugin-free declarative reservoir and outlet declarative.yaml

Configuration

run.yaml is responsible for building the scene. It declares the environment, articulated kettle arm, two fluid pools, and the RGB camera. The pools are separate because they have different capacity and appearance:

  • fluids.pipe_coffee: light coffee, 18,000 particles;
  • fluids.kettle_water: near-transparent water, 8,000 particles.

color_rgba belongs to each fluid component. Changing one liquid does not recolor the other, and it does not alter the physical density, viscosity, or surface tension.

declarative.yaml shows the smaller configuration-only path. reservoirs and emitters are optional Core fields nested under a fluid entity; Runtime creates them when the Run is prepared and restores them on reset. No plugin or Python setup code is involved.

Continuous stream and spray

The pipe uses one axis for every particle velocity:

python
FluidEmitterRequest(
    emitter_id="pipe-coffee",
    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,
)

The kettle outlet follows one authored articulation link. Its particles receive deterministic directions distributed inside a 40-degree cone:

python
FluidEmitterRequest(
    emitter_id="kettle-spout",
    fluid_id="fluids.kettle_water",
    attachment=FluidAttachment(
        parent="robots.pour_arm",
        link_name="kettle_link",
        position_m=(0.405, 0.0, 0.085),
        direction_local=(1.0, 0.0, 0.0),
    ),
    reservoir_id="kettle-reservoir",
    flow_rate_ml_s=22.0,
    spray_half_angle_rad=math.radians(40.0),
    trigger=FluidTrigger.gravity_pour(
        start_angle_deg=28.0,
        stop_angle_deg=38.0,
    ),
)

spread_radius_m controls the size of the outlet opening. It does not create a spray. spray_half_angle_rad controls directional dispersion; zero means a coherent stream.

The seed makes the spray repeatable. The same Run and seed generate the same initial particle directions.

Reservoir and trigger behavior

The kettle receives 120 ml from a 150 ml reservoir. It starts in armed state. The arm moves pour_joint; once the spout direction crosses the gravity-pour threshold, the emitter becomes active. It stops when the reservoir can no longer fund another particle.

Run it

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
# Run the configuration-only variant without installing a liquid plugin:
fastsim run declarative.yaml --project ../.fastsim/project.yaml
python main.py

The program opens a visible Isaac Lab window and writes:

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

The MP4 comes from the scene camera, not a desktop or window recorder.

Record → Replay

FastSim Core can expose each accepted, non-empty emission batch through the pay-for-play fluid.audit service. Record 0.3.8 writes those compact deterministic inputs to the fluid.emissions FSR stream; it does not copy every active particle on every frame. Replay 0.2.15 loads the complete stream through fluid.replay during prepare, disables the configured live emitters, and injects each reconstructed batch before physics at its recorded tick. Physics then evolves the particles normally.

This demo's offline Project intentionally contains only the shared component catalog and has installed_packages: false. Record/Replay Run files are therefore not checked in here until those exact plugin releases are present in the Project catalog.

Planning geometry

Particle fluids never enter planning.scene geometry. A planner sees the pipe, kettle, arm, and floor collision geometry, but not thousands of short-lived liquid particles. Particle state remains available through the dedicated fluid query API.

Backend note

This case requires native particle-fluid simulation and is currently validated with the Isaac Lab adapter. Other backends must declare the same fluid capabilities before this Run can select them.

Verification level

REAL VISIBLE ISAAC PASS. The merged case completed with 17,600 pipe particles, 7,600 kettle particles, a completed kettle emitter, and 1.25 ml remaining in the logical reservoir. The scene camera produced a 1280×720, 24 FPS, 310-frame MP4.