Pipeline choice

GLSL vs TSL

The same fluid concepts in two render pipelines: WebGL EffectComposer passes and WebGPU RenderPipeline nodes.
WebGL and WebGPU

The library has two public entries because three.js now has two practical rendering paths.

import { FluidSimulation } from 'three-fluid-fx'     // WebGL / GLSL
import { FluidSimulation } from 'three-fluid-fx/tsl' // WebGPU / TSL

The solver surface is intentionally similar. The composition layer is what changes.

WebGL / GLSL

Use the default three-fluid-fx entry when your project uses WebGLRenderer, EffectComposer, and post-processing passes.

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
import { FluidSimulation, OilOverlayPass } from 'three-fluid-fx'

const fluid = new FluidSimulation(renderer)
const overlay = new OilOverlayPass(fluid)

const composer = new EffectComposer(renderer)
composer.addPass(new RenderPass(scene, camera))
composer.addPass(overlay)
composer.addPass(new OutputPass())

renderer.setAnimationLoop(() => {
  fluid.step(1 / 60)
  overlay.time = clock.getElapsed()
  composer.render()
})

Use this path when:

  • You already have a WebGL post-processing stack.
  • You want drop-in passes.
  • You need the widest browser compatibility.

WebGPU / TSL

Use three-fluid-fx/tsl when your project uses WebGPURenderer, RenderPipeline, and TSL node graphs.

import { RenderPipeline, WebGPURenderer } from 'three/webgpu'
import { pass, uniform } from 'three/tsl'
import { FluidSimulation, oilOverlay } from 'three-fluid-fx/tsl'

const renderer = new WebGPURenderer({ antialias: true, forceWebGL: false })
await renderer.init()

const fluid = new FluidSimulation(renderer)
const sceneNode = pass(scene, camera)
const time = uniform(0)

const pipeline = new RenderPipeline(renderer)
pipeline.outputNode = oilOverlay(
  sceneNode,
  fluid.densityNode,
  fluid.dyeNode,
  fluid.velocityNode,
  { time },
)

renderer.setAnimationLoop(() => {
  time.value = clock.getElapsed()
  fluid.step(1 / 60)
  pipeline.render()
})

Use this path when:

  • You are already building with WebGPU and TSL.
  • You want composition as a node graph.
  • Your target browser/runtime has WebGPU.

Concept mapping

Parameter What it controls How to tune it
FluidSimulation Same conceptual solver in both entries. GLSL uses WebGL render targets. TSL uses a WGSL-backed implementation and StorageTexture outputs.
attachPointerSplats Same pointer helper for both pipelines. It depends only on splatForce and addSplat, not on a concrete class.
velocityTexture / velocityNode Fluid output for downstream motion. Use texture in WebGL shaders. Use node in TSL graphs.
densityTexture / densityNode Visual flow plus density mask. Most overlays and distortions read this output.
Pass classes / node functions Composition API. GLSL exports classes like OilOverlayPass. TSL exports functions like oilOverlay().

Style switching

In WebGL, create pass instances and toggle enabled.

const passes = {
  oil: new OilOverlayPass(fluid),
  smoke: new SmokeOverlayPass(fluid),
}

for (const pass of Object.values(passes)) composer.addPass(pass)

function setStyle(style: keyof typeof passes) {
  for (const pass of Object.values(passes)) pass.enabled = false
  passes[style].enabled = true
}

In TSL, rebuild the output node when the selected style changes.

function setStyle(style: FluidOverlayStyle) {
  setPipelineOutput(
    pipeline,
    fluidOverlay(style, sceneNode, fluid.densityNode, fluid.dyeNode, fluid.velocityNode, options),
  )
}

Parameter semantics stay the same

The visual tuning words do not change between GLSL and TSL:

  • splatRadius is still brush size.
  • splatForce is still pointer force.
  • curlStrength still adds vortex energy when enableVorticity is on.
  • densityDissipation still controls the mask lifetime.
  • dyeDissipation still controls colored stroke lifetime.
  • intensity, vibrance, and time still belong to the visual effect.