| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| Practical Example | Practical Example |
| Reference | Reference |
WaterBody2D and WaterBody3D add a simulated water surface that renders, runs a GPU height simulation, pushes buoyancy on rigid bodies, and reports enter/exit overlaps like an area. One node covers the look of the water, the float physics, and the "is the player in the water" question for pools, rivers, lakes, and ocean patches.
- Floating and drifting props (barrels, boats, debris): drop a
WaterBody3Dand tunebuoyancy,drag, andflow; eachRigidBody3D'sdensitysets how high it rides. - Swim state, drowning damage, or muffled audio when a character is submerged: connect the water's
Entered/Occupied/Exitedsignals (named<WaterNodeName>_Entered, etc., likeArea2D/Area3D) withsignal_connect!. - Rivers that carry objects downstream:
idle_mode = "river"with a non-zeroflow. - Splashes from blasts and abilities: a
PhysicsForceEmitter3D/PhysicsForceEmitter2Dwithaffect_water = trueturns its force events into wakes. - Natural shorelines and banks: static collision shapes that pass the water mask cut coastline holes and damp waves against the edge.
Use a water body when one region needs the surface, overlap state, and buoyancy to agree. Use an Area2D / Area3D plus a visual effect when gameplay only needs an enter/exit volume; that avoids wave simulation and buoyancy. Keep swim rules in a script that listens to water signals, while the water node owns fluid behavior.
A lake with a wooden crate that floats. The WaterBody3D supplies the surface and buoyancy; the crate is an ordinary RigidBody3D whose density controls buoyancy.
[Lake]
[WaterBody3D]
shape = { type="cube", size=(64, 8, 64) }
idle_mode = "chop"
buoyancy = 1.5
drag = 0.35
[Node3D/]
[/WaterBody3D]
[/Lake]
[Crate]
[RigidBody3D]
density = 0.6
[Node3D]
position = (0, 6, 0)
[/Node3D]
[/RigidBody3D]
[/Crate]
[CrateShape]
parent = @Crate
[CollisionShape3D]
shape = { type = cube, size = (1, 1, 1) }
[/CollisionShape3D]
[/CrateShape]
The crate drops, sinks until buoyancy balances gravity, then bobs with the surface. Lower density floats higher; raise it above the water's effective density and the crate sinks.
WaterBody2D and WaterBody3D define simulated water surfaces.
They render water, run a GPU height simulation, and feed buoyancy forces into rigid bodies during fixed physics.
Use water bodies for pools, rivers, lakes, ocean patches, or gameplay zones where bodies should float and slow down.
2D water uses Node2D transform data.
The water surface uses shape around the node position.
Height is along world y.
[Pond]
[WaterBody2D]
shape = { type="quad", width=64, height=24 }
quality = "medium"
depth = 5.0
flow = (0.5, 0)
wind = (1, 0)
idle_mode = "sine"
wave_speed = 1.2
wave_scale = 0.6
damping = 0.98
buoyancy = 2.0
drag = 0.45
wake_strength = 1.4
foam_strength = 0.7
deep_color = (0.02, 0.16, 0.28, 0.94)
shallow_color = (0.08, 0.46, 0.62, 0.74)
shallow_depth = 8.0
sample_readback_rate = 30
collision_layers = all
collision_mask = none
coastline = { foam_color=(0.9, 0.97, 1.0, 1.0) foam_strength=0.75 foam_width=1.5 cutoff_softness=0.25 wave_reflection=0.45 wave_damping=0.35 edge_noise=0.2 }
debug = false
[Node2D]
position = (0, 0)
z_index = 0
visible = true
[/Node2D]
[/WaterBody2D]
[/Pond]
3D water uses Node3D transform data.
The water surface uses shape in local x/z.
Height is world y.
[Lake]
[WaterBody3D]
shape = { type="cube", size=(128, 12, 128) }
quality = "high"
depth = 12.0
flow = (0, 0.25)
wind = (1, 0)
idle_mode = "chop"
wave_speed = 1.0
wave_scale = 1.0
damping = 0.985
buoyancy = 1.5
drag = 0.35
wake_strength = 1.35
foam_strength = 0.9
optics = { deep_color=(0.02, 0.16, 0.28, 0.94) shallow_color=(0.08, 0.46, 0.62, 0.74) sky_bias={ ratio=0.35 } }
[Node3D]
position = (0, 0, 0)
visible = true
[/Node3D]
[/WaterBody3D]
[/Lake]
shape: water bounds. 2D acceptsrect/quadandcircle. 3D acceptscube/box,cylinder, orsphereas a cylinder shortcut.- 2D quad/rect surface axes are local
x/y. - 3D box/cylinder surface axes are local
x/z; height/depth is local/worldy. quality(aliaseswater_quality,fidelity): the single fidelity knob. Accepts"low","medium","high", or"ultra"; value aliases arefast/lowestfor low,mid/medfor medium, andmax/highestfor ultra. Defaults to"low", so authors opt in to more detail.- The tier is a target triangle edge length in screen pixels: low is about 32px, medium about 20px, high about 12px, ultra about 8px. The engine derives tessellation per render chunk each frame from chunk distance, camera projection, and render-target height, so one tier gives the same on-screen triangle density regardless of body size, camera distance, window size, or
render_scale. - The tier also sets the GPU simulation grid per axis (low
64x64, medium96x96, high160x160, ultra256x256) and the defaultsample_readback_rate(low10, medium20, high30, ultra60). - 2D water renders as a screen quad, so in 2D
qualityonly changes the simulation grid and readback rate. depth: visual/physics water depth hint.flow: water current in surface-local axes.wind: wave direction for idle modes.idle_modeoridle:"calm","sine","chop"/"choppy","storm", or"river". River mode rushes alongflow; ifflow = (0, 0), it falls back towind.wave_speed: idle wave time scale.1is a slow default; old fast motion is closer to5.wave_scale: idle wave height scale.wave_length,wavelength, orwave_size: world-space wave profile length in meters. Defaults do not scale wave size from water body bounds.chopandstormlayer several world-space wave directions so large water does not become one broad sine sheet.stormalso adds moving steep swell peaks for rough water.damping: simulation damping, clamped to0..1.buoyancy: upward force multiplier for rigid bodies inside the surface bounds.drag: vertical velocity damping applied while submerged.wake_strength: wake impulse scale used by the water simulation.foam_strength: simulation foam response scale.sample_readback_rateorreadback_rate: target GPU sample readback rate.qualitypicks the default; an explicit value overrides the tier default. Renderer uses the max requested rate across visible water bodies.deep_colorandshallow_color: water color/opacity endpoints. Surface color derives between them from depth, waves, Fresnel, and refraction tint. Shallow alpha should usually be lower than deep alpha, but default water stays mostly opaque.shallow_depth: visual depth cutoff where water finishes fading from shallow color/alpha toward deep color/alpha.-1uses the automatic old scale. Use larger values for fish tanks or clear pools that should stay see-through.sky_bias: optional activeSky3Dcolor pull. Usesky_bias = "none",sky_bias = 0.0, orsky_bias = { ratio=0.35 }.optics = { ... }accepts the same color,shallow_depth, and sky fields.materialorvisual: WaterMaterial-style render knobs:transparency,reflectivity,roughness,fresnel_power,normal_strength,ripple_scale,foam_color,foam_amount,crest_foam_threshold,caustic_strength,refraction_strength,scattering_strength, anddistance_fog_strength.collision_layers: water sensor tagged layers. Defaults to all layers.collision_mask: tagged layers water ignores for buoyancy, wakes, and coastline. Defaults to no layers.link_layers: water link layers. Defaults to all layers.link_mask: water link layers ignored for automatic cross-body blending. Defaults to no layers.blend_width: explicit overlap blend width.0picks an automatic cubic blend width from the overlap size.wave_transfer: wave transfer multiplier across linked water. Defaults to1. Foam transfer fields stay compatible, but 3D visual foam is disabled.flow_transfer: flow velocity transfer multiplier across linked water. Defaults to1.coastline: static-body shoreline cut settings. Foam/color outline fields stay compatible, but 3D visual foam/outlines are disabled.debug: enable debug water view.
Defaults:
WaterBody2D:shape = { type="quad", width=32, height=32 },quality = "low",depth = 4.WaterBody3D:shape = { type="cube", size=(500, 35, 500) },quality = "low",depth = 35.- Shared defaults:
shallow_depth = -1,sky_bias = "none",sample_readback_rate = 10(from the"low"tier),collision_layers = all,collision_mask = [],link_layers = all,link_mask = [],blend_width = 0,wave_transfer = 1,flow_transfer = 1.
Removed fields:
The old fidelity knobs are gone; quality replaces all of them. Scenes that still set one get a [perro][runtime] warning at scene load, the field does not resolve, and perro doctor reports it as an error.
- Absolute grid sizes:
resolution,sim_resolution,render_resolution,mesh_resolution. - Per-meter densities:
vertices_per_meter,verts_per_meter,vpm,resolution_per_meter,sim_vertices_per_meter,sim_cells_per_meter,simulation_cells_per_meter,render_vertices_per_meter,render_verts_per_meter,mesh_vertices_per_meter. - LOD distance bands and floors:
lod_near_distance/lod_near,lod_mid_distance/lod_mid,lod_far_distance/lod_far,lod_min_resolution,min_resolution.
Buoyancy force falloff with camera distance is now a fixed engine constant (128/384/896 meter bands) instead of a per-body setting.
The GPU simulates water cells inside the water shape bounds, on the grid the quality tier picks.
The simulation grid never changes with camera distance, so XZ height samples and buoyancy stay stable while render detail moves.
Intersecting water bodies auto-link when link layers/masks allow it.
Linked bodies keep separate simulation grids, but overlap samples use a cubic blend for surface height, flow, buoyancy, and wake transfer.
Water meshes are split into render chunks derived from body world size: about 12 world units per chunk, up to 8 chunks per axis.
Each chunk picks its own LOD from its own distance to the camera, so the far half of a large body tessellates coarser than the near half.
Adjacent chunks never crack: the LOD ratio between neighbours is capped at 4x, and the finer chunk snaps its boundary vertices onto the coarser neighbour's vertices.
Because the target is a triangle edge in screen pixels, chunk detail already accounts for camera distance, window size, and render_scale.
3D mid/far water uses a cheaper shader path for lower GPU cost.
Water samples are read back from the GPU for physics. If no GPU sample is ready, physics uses an analytic idle wave fallback from the same water settings. This keeps physics deterministic enough to run even when GPU readback lags.
Water bodies create sensor colliders.
They do not block motion, raycasts, or contact pairs.
They emit WaterNodeName_Entered, WaterNodeName_Occupied, and WaterNodeName_Exited like Area2D/Area3D.
- Runtime finds all
WaterBody2DandWaterBody3Dnodes. - Runtime tests rigid body centers against each water shape.
- Runtime samples surface height at the body local point.
- Runtime scales the force by water LOD distance from the active camera.
- If the body center is below the sampled surface, runtime queues an upward force plus vertical drag when force is above the LOD deadzone.
- Normal physics force/impulse application and world stepping run after that.
Physics LOD, using fixed engine distance bands of 128, 384, and 896 meters:
- Near: full force, no deadzone.
- Mid: force fades to
0.75x, small deadzone. - Far: force fades to
0.4x, larger deadzone. - Beyond far:
0.25xforce,0.5deadzone.
2D water affects RigidBody2D.
It uses body density in the buoyancy calculation.
3D water affects RigidBody3D.
It uses body density in the buoyancy calculation.
Static bodies are not moved by buoyancy. Static collision shapes that pass the water/body mask test cut coastline holes and damp waves. 3D shoreline foam/outlines are disabled for now.
Physics force emitters also affect water.
PhysicsForceEmitter2D and PhysicsForceEmitter3D send nearby force events into water when affect_water = true.
Water converts those events into wakes and a cavitation scalar. 2D still uses foam; 3D visual foam is disabled for now.
Explosion, lift, current, vortex, and custom force profiles all use the same water interaction path.
Water owns surface simulation, visual state, sensor overlap, wake parameters, LOD, coastline masking, and buoyancy sampling. Static/rigid bodies keep owning solid collision and contact behavior.
This keeps common authoring simple:
- Add water node for visual water, sensor overlap, and float force.
Run a focused water case with a stable capture window:
$env:PERRO_GPU_BENCH = "water_sim_1_64"
$env:PERRO_GPU_BENCH_THROUGHPUT = "1"
$env:PERRO_GPU_CAPTURE_MS = "5000"
$env:PERRO_GPU_BENCH_CSV = "target/water-gpu-bench.csv"
cargo bench -p perro_graphics --bench gpu_framePERRO_GPU_CAPTURE_MS keeps the final rendered frame visible for capture.
PERRO_GPU_BENCH_THROUGHPUT queues frames like the uncapped runtime and waits for the GPU once after the sample batch. Without it, the benchmark waits after every frame for isolated latency measurements.
PERRO_GPU_BENCH_CSV appends CPU, GPU-main, GPU-water, draw-call, and instance timing data.
Use water_sim, water_idle, or another case substring to select the workload.
- Add static collider nodes for solid banks, floor, rocks, docks, and islands.
- Tune
buoyancy,drag, andflowfor feel without editing body shapes.