Start

Getting Started

A compact integration path for the solver, pointer splats, outputs, resize handling, and the parameters you actually tune.
Core API

three-fluid-fx is not a visual preset engine. It gives you a small fluid field that can be sampled from three.js. The field is usually driven by pointer movement, then consumed by an overlay, a distortion pass, or your own particle system.

The smallest useful integration

Create a renderer, create the fluid simulation, attach pointer splats, and step the solver in the render loop.

import { WebGLRenderer, Timer } from 'three'
import { attachPointerSplats, FluidSimulation } from 'three-fluid-fx'

const renderer = new WebGLRenderer({ antialias: true })
const fluid = new FluidSimulation(renderer, {
  profile: 'balanced',
  splatRadius: 0.001,
  splatForce: 6,
})

const detachPointer = attachPointerSplats(renderer.domElement, fluid)

const clock = new Timer()
renderer.setAnimationLoop(() => {
  clock.update()
  fluid.step(clock.getDelta())

  // Render your scene, effect composer, custom shader, or particles here.
})

Keep resize explicit

The simulation uses internal render targets. Resize them with the canvas so the field keeps the same aspect ratio as the viewport.

function resize() {
  const width = stage.clientWidth
  const height = stage.clientHeight
  const dpr = Math.min(window.devicePixelRatio || 1, 2)

  renderer.setPixelRatio(dpr)
  renderer.setSize(width, height, false)
  fluid.resize(width, height)
}

resize()
window.addEventListener('resize', resize)

The profile controls the baseline render-target sizes. resize() reshapes those targets to match the viewport aspect. It does not change the selected profile.

What the library gives you

The main output is not a mesh. It is a set of textures in the GLSL pipeline and matching TextureNodes in the TSL pipeline.

Output Channels Use it for
fluid.velocityTexture .xy Post-advection velocity. Use it for particle systems and downstream motion.
fluid.velocityProjectedTexture .xy Projection-clean velocity before self-advection. Use it for cleaner velocity visualizations.
fluid.densityTexture .rg + .b Display-space smeared flow in RG and density mask in B. Distortion and most overlays use this.
fluid.dyeTexture .rgb Per-stroke color. Requires fluid.enableDye = true and dyeColor splats.

For WebGPU/TSL, the names mirror the texture names:

fluid.velocityNode
fluid.densityNode
fluid.dyeNode

Use textures with WebGL materials and post-processing passes. Use nodes with WebGPURenderer and RenderPipeline.

Pointer splats

Pointer splats are small impulses written into the velocity and density fields. The helper is intentionally thin: it reads fluid.splatRadius and fluid.splatForce every pointer event.

fluid.splatRadius = 0.0012
fluid.splatForce = 8

You can also add splats manually. Coordinates are normalized from 0 to 1.

fluid.addSplat(0.5, 0.5, 120, 40, {
  radius: 0.001,
  color: [120, 40, 1],
})

For colored ink effects, turn on the dye field and write dyeColor splats.

fluid.enableDye = true

attachPointerSplats(renderer.domElement, fluid, {
  coloredStrokes: true,
})

Use colorize when the stroke color should be deterministic instead of random HSV cycling.

attachPointerSplats(renderer.domElement, fluid, {
  colorize(dx, dy) {
    const mag = Math.min(Math.hypot(dx, dy) / 80, 1)
    return [Math.abs(dx) * 0.003 * mag, 0.08 * mag, Math.abs(dy) * 0.003 * mag]
  },
})

Parameters that change the look

You do not need to teach users the Stable Fluids algorithm. You do need to tell them which controls make a visual difference.

Parameter What it controls How to tune it
splatRadius Brush size in normalized UV units. Small values make sharp streaks. Larger values make soft blobs, water surfaces, and smoke.
splatForce Pointer motion gain before the splat is written into velocity. Raise it for punchy smears and particles. Lower it for slow ink and glass.
pressureIterations How much projection cleanup runs each step. Higher values look tighter and cost more. Lower values relax outward and feel softer.
curlStrength + enableVorticity Extra vortex energy added back into the field. Use it for oil, smoke, chromatic, and ink. Disable it for clean trails.
velocityDissipation How long motion remains alive. Closer to 1 means motion keeps pushing. Lower values calm the field quickly.
densityDissipation How long the visible density mask remains alive. Closer to 1 gives long trails and persistent masks. Lower values make quick puffs.
dyeDissipation How long colored per-stroke dye remains alive. Set higher than density for watercolor and ink whose color outlives the motion.
bfecc Sharper advection path. Leave on for crisp trails. Turn off for softer diffusion when a style wants bloom.
reflectWalls Whether flow bounces from the screen edges. Use true when you want energy to stay on screen. Use false when smoke or ink should drift away.

Profiles

Profiles pick the texture sizes and the default pressure iteration count at construction time.

new FluidSimulation(renderer, { profile: 'performance' }) // weak GPUs, small embeds
new FluidSimulation(renderer, { profile: 'balanced' })    // default
new FluidSimulation(renderer, { profile: 'quality' })     // hero visuals, capture

Individual options still override the selected profile.

const fluid = new FluidSimulation(renderer, {
  profile: 'balanced',
  pressureIterations: 8,
})

Pick the render path

Use the same solver for every visual family.

// Overlay: add density, dye, or velocity color over the scene.
scene.rgb + color * density * intensity

// Distortion: use fluid flow to shift scene UVs.
texture2D(tDiffuse, uv - fluid.rg * intensity)

// Particles: sample velocity at each particle screen position.
particleAcceleration += velocityField.xy * flowStrength
The Hello World WebGL example renders density directly so the field is visible. Open example