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:
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:
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
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:
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.