--- title: "Particles Guide" description: "How to use the fluid field with procedural particles and GPGPU particles, why camera data is passed, and how to tune the motion without ambiguity." section: "Particles" order: 3 badge: "Procedural and GPGPU" --- import Callout from '../../components/tutorials/Callout.astro' import ExampleEmbed from '../../components/tutorials/ExampleEmbed.astro' import FlowDiagram from '../../components/tutorials/FlowDiagram.astro' import ParameterTable from '../../components/tutorials/ParameterTable.astro' The library does not ship a particle engine. It gives you a flow field. A particle system becomes fluid-reactive when it samples that field and turns the sample into displacement or acceleration. There are two useful patterns in this repository: - Procedural particles: no persistent particle simulation. Positions are computed from a shape formula and displaced at render time. - GPGPU particles: position and velocity live in GPU textures. A compute or fragment update pass advances them every frame. ## The contract from the fluid side For particles, the core contract is small: ```ts fluid.step(dt) particles.step({ velocityField: fluid.velocityTexture, // particle-specific data... }) ``` `velocityTexture.xy` stores the flow after the fluid step. That is the texture you want when particles should move with the simulated field. A particle component should accept a `Texture`, not a `FluidSimulation` instance. That keeps it reusable with curl-noise textures, SDF gradient textures, video-driven fields, or any other 2D vector map. ## Procedural particles The trefoil example is the procedural path. It does not maintain particle position and velocity textures. Instead, each instance computes its base position from a knot formula, samples the fluid, and offsets the render-time position. ```ts const particles = createTrefoilParticles(fluid.velocityNode, { count: 4000, tubeRadius: 0.22, scale: 0.65, pointSize: 7, }) scene.add(particles.mesh) ``` The update path only changes uniforms: ```ts particles.update({ elapsed, modelRotation, displacement, dispThreshold, dispRange, dragStrength, maxFlowSpeed, }) ``` ### What is happening The particle does not have memory. There is no integrated velocity from the previous frame. Every frame starts from a clean procedural position: ```glsl vec3 base = trefoilPosition(instanceId); FluidSample sample = fluidSample(fluid, base); vec3 displaced = base + sample.flow * uDisplacement; ``` This is best when the visual has a strong underlying shape: a knot, ribbon, sphere shell, grid, portrait mask, typography, or any procedural field. The fluid acts like a brush that bends the shape. ### Procedural particle controls ## GPGPU particles The 2D and 3D flow-particle examples are GPGPU systems. They store persistent state on the GPU: Each frame, the particle update reads old position, old velocity, destination, and the fluid field. It writes new position and velocity into the next ping-pong targets. ```ts particles.step({ dt, velocityField: fluid.velocityTexture, viewMatrix: camera.matrixWorldInverse, projectionMatrix: camera.projectionMatrix, cameraRight, cameraUp, modelRotation, pointSize: 6, spring: 4, zeta: 1.15, dragLin: 0.28, dragQuad: 0.05, aMax: 24, vMaxScale: 1, flowStrength: 1.2, flowThreshold: 0.02, maxFlowSpeed: 12, responseGamma: 2, depthLift: 0, perpendicularAngle: 0, sideVariation: 0, depthAttenuationScale: 1, }) ``` ## Why camera data is passed This is the part that should be explicit in the tutorial. The fluid field is a 2D screen-space texture. The particle positions are in local or world space. The particle update has to answer one question: > Which pixel of the fluid field is behind this particle on screen? That requires the same projection path used by rendering: ```glsl vec3 worldPos = uModelRotation * pos; vec4 clip = uProjectionMatrix * uViewMatrix * vec4(worldPos, 1.0); vec2 ndc = clip.xy / clip.w; vec2 uv = ndc * 0.5 + 0.5; vec2 flow = texture2D(uFlow, clamp(uv, 0.0, 1.0)).xy; ``` Then the sampled flow is still a screen-space vector. `flow.x` means "move right on the screen", and `flow.y` means "move up on the screen". Particles need a world acceleration, so the update converts screen axes back into world directions: ```glsl vec3 flowWorld = flow.x * uCameraRight + flow.y * uCameraUp; ``` That is why the step call needs: Typical frame setup: ```ts particles.points.updateMatrixWorld(true) modelRotation.setFromMatrix4(particles.points.matrixWorld) cameraRight.setFromMatrixColumn(camera.matrixWorld, 0) cameraUp.setFromMatrixColumn(camera.matrixWorld, 1) particles.step({ viewMatrix: camera.matrixWorldInverse, projectionMatrix: camera.projectionMatrix, cameraRight, cameraUp, modelRotation, velocityField: fluid.velocityTexture, // ... }) ``` ## Why particles do not fly away forever The example particle system combines flow acceleration with a spring-damper that pulls each particle back toward its destination. ```glsl vec3 error = destination - position; vec3 aSpring = spring * spring * error; vec3 aDamp = -2.0 * zeta * spring * velocity; ``` Without this, a single pointer gesture eventually scatters the cloud. With it, the cloud can react, drift, and settle back into its designed layout. ## Flow response controls Fluid velocity values can be noisy and uneven. The particle example shapes that input before applying it. ## 2D plane vs 3D cloud The 2D plane uses the same screen-space flow projection but keeps depth shaping off: ```ts particles.step({ mode: 'plane2d', depthLift: 0, perpendicularAngle: 0, sideVariation: 0, }) ``` The 3D cloud starts particles on a sphere-like layout and uses extra controls to make a 2D flow texture feel volumetric. ```ts particles.step({ mode: 'cloud3d', depthLift: 0.65, perpendicularAngle: 0.75, sideVariation: 0.35, }) ``` `depthLift` adds motion along depth. `perpendicularAngle` introduces side motion around the screen-flow direction. `sideVariation` varies that side motion per particle so the cloud does not move as one flat sheet. ## Which particle path should you use? Use procedural particles when: - The final shape is more important than physical continuity. - You want a knot, mask, surface, text shape, or field to react to the brush. - You want fewer moving parts and no persistent particle state. Use GPGPU particles when: - The particles should carry momentum between frames. - You need a cloud, dust, sparks, confetti, or a sheet that drifts and settles. - You want spring, damping, drag, and flow-response controls.