| title | Effects Guide |
|---|---|
| description | A practical catalog of overlays and distortions: what each family reads, when to use it, and which parameters make it look good. |
| section | Visual effects |
| order | 2 |
| badge | Overlay and distortion catalog |
import Callout from '../../components/tutorials/Callout.astro' import EffectCard from '../../components/tutorials/EffectCard.astro' import ExampleEmbed from '../../components/tutorials/ExampleEmbed.astro' import ParameterTable from '../../components/tutorials/ParameterTable.astro' import RecipeCard from '../../components/tutorials/RecipeCard.astro' import TextureChannels from '../../components/tutorials/TextureChannels.astro'
The effect classes do not own the fluid. The solver runs once, then an effect decides how to display the latest textures.
In WebGL, effects are EffectComposer passes:
const overlay = new OilOverlayPass(fluid)
composer.addPass(new RenderPass(scene, camera))
composer.addPass(overlay)
composer.addPass(new OutputPass())In WebGPU/TSL, effects are node factories:
pipeline.outputNode = oilOverlay(
sceneNode,
fluid.densityNode,
fluid.dyeNode,
fluid.velocityNode,
{ intensity, time, vibrance },
)Overlay and distortion effects usually read densityTexture. That name is a
little misleading: RG stores a display-space flow vector and B stores density.
Dye-aware overlays also read dyeTexture.
Overlay effects add or mix color over the scene. Use them when the fluid should be visible as a layer rather than bending the pixels underneath.
Good for product sites and hero backgrounds where the user expects a clean cursor trail.
<EffectCard title="Trail" family="Directional wake" reads="densityTexture" params="intensity, cursorColor, low curl, reflectWalls"
<p>Use for sharp leading edges and long tails. Keep vorticity off when the streak should stay clean.</p>
<EffectCard title="Oil / Colorful / Rainbow Fish" family="Velocity stylization" reads="densityTexture or velocityTexture" params="time, vibrance, curlStrength, velocityDissipation"
<p>Best when visible swirls matter. Raise curl and keep motion alive near 0.985 to 0.99.</p>
<EffectCard title="Smoke / Art Ink / Rainbow Ink" family="Dye-aware strokes" reads="dyeTexture, densityTexture" params="enableDye, coloredStrokes, dyeDissipation, intensity"
<p>Use when stroke color matters. The dye field is separate, so color can live longer than the mask.</p>
<EffectCard title="Color Water" family="Soft watercolor" reads="dyeTexture, densityTexture" params="low pressure, high dyeDissipation, open walls"
<p>Let the field relax outward. This wants soft diffusion rather than tight vortex motion.</p>
<EffectCard title="Liquid Lens" family="Tinted lens overlay" reads="dyeTexture, densityTexture" params="colorize, bfecc false, intensity below bright overlays"
<p>Use when the stroke should feel like tinted glass or a liquid cursor lens.</p>
Distortion effects keep the scene color but shift where the scene is sampled. Use them for glass, heat haze, refraction, melting UI, and water surfaces.
The cheapest option. It simply offsets scene UVs by the fluid flow.
<EffectCard title="RGBShiftDistortionPass / rgbShiftDistortion" family="Chromatic split" reads="densityTexture.rg and densityTexture.b" params="intensity, splatForce, enableVorticity"
<p>Use when motion should split red and blue channels along the flow direction.</p>
<EffectCard title="ChromaticDistortionPass / chromaticDistortion" family="Iridescent smear" reads="blurred densityTexture" params="low visual intensity, high densityDissipation, vorticity"
<p>Good for oil-slick refraction. The effect is strong, so start with low intensity.</p>
<EffectCard title="WaterDistortionPass / waterDistortion" family="Density-as-height" reads="densityTexture.b" params="larger splats, slow density decay, gentle curl"
<p>Uses the density gradient as a fake water normal. Flat regions stay sharp.</p>
<EffectCard title="WaterCausticsDistortionPass / waterCausticsDistortion" family="Water plus light web" reads="densityTexture.rg and densityTexture.b" params="time, intensity, active density mask"
<p>Adds a procedural caustic web over the water refraction. Pass time every frame.</p>
These are starting points, not sacred values. Copy a recipe, then tune by eye.
Use `TrailOverlayPass`, small `splatRadius`, medium `splatForce`, `enableVorticity = false`, and `reflectWalls = true`.
Use `SmokeOverlayPass`, larger splats, slow density decay, vorticity on, and `reflectWalls = false` so plumes leave the screen.
Use `ArtInkOverlayPass` or `RainbowInkOverlayPass`, set `fluid.enableDye = true`, and attach pointer splats with colored strokes.
Use `ColorWaterOverlayPass`, low pressure iterations, high `dyeDissipation`, open walls, and gentle curl.
Use `SimpleDistortionPass`, low to medium intensity, and tune `splatForce` until motion is visible but not tearing the scene.
Use `WaterDistortionPass` or `WaterCausticsDistortionPass`, larger soft splats, slow density decay, and time for caustics.
<ParameterTable rows={[ { name: 'intensity', role: 'Visual gain inside the pass or node.', tune: 'Overlay intensity can often be above 1. Distortion intensity should be lower because UV offsets can tear.', }, { name: 'vibrance', role: 'Saturation boost away from luminance.', tune: 'Use small values for production UI. Push higher for demo and reel visuals.', }, { name: 'cursorColor', role: 'Base color for cursor-tinted overlays.', tune: 'Used by default, volume cursor, and trail styles.', }, { name: 'time', role: 'Animation clock for procedural palettes and caustics.', tune: 'Update once per frame. Required for oil, colorful, rainbow fish, burn, and caustics.', }, { name: 'opacity', role: 'TSL overlay mix amount.', tune: 'Useful when stacking overlay and distortion in the same RenderPipeline output.', }, { name: 'velocityScale', role: 'TSL-only gain for effects that read velocityTexture.', tune: 'Lower it for velocity visualization and rainbow fish when the field is too hot.', }, ]} />
Create all WebGL passes once, add them to the composer, and toggle enabled.
Disabled passes are skipped.
const overlays = {
trail: new TrailOverlayPass(fluid),
oil: new OilOverlayPass(fluid),
smoke: new SmokeOverlayPass(fluid),
}
for (const pass of Object.values(overlays)) {
pass.enabled = false
composer.addPass(pass)
}
function syncOverlay(style: keyof typeof overlays) {
const active = overlays[style]
for (const pass of Object.values(overlays)) pass.enabled = pass === active
active.intensity = params.intensity
if ('time' in active) active.time = elapsed
}For TSL, rebuild the output node when the style changes.
function buildOutput(style: FluidOverlayStyle) {
return fluidOverlay(style, sceneNode, fluid.densityNode, fluid.dyeNode, fluid.velocityNode, {
intensity,
time,
cursorColor,
vibrance,
})
}
setPipelineOutput(pipeline, buildOutput(params.overlayStyle))