# GPU effects

> TypeGPU effects on three's own GPU device, Particles, Trail, Grass, LivelyGround, Water and GpuStrip, and how to write your own compute shader that fills buffers three draws.

The GPU folder (`src/engine/gpu/`, exported from `@/engine`) holds effects that move thousands of things without JavaScript per thing. They all follow one split: **three draws, TypeGPU simulates.** A TypeGPU compute shader, written in TypeScript with the `'use gpu'` directive, fills buffers that three's `WebGPURenderer` draws from in the same frame, on the same GPU device.

Use these for decoration that reacts to the game: fire, sparks, rain, magic, trails, a meadow, water. Gameplay never reads GPU state back: collisions and scoring stay in JavaScript.

## Smallest working example

A campfire: fire, smoke above it and sparks you can burst on demand.

```tsx
import React, { useRef } from 'react';
import { EngineCanvas, Environment, Particles, type ParticlesHandle } from '@/engine';

export const Campfire = () => {
  const sparks = useRef<ParticlesHandle>(null);
  // from gameplay: sparks.current?.burst(60, [0, 0.3, 0]);
  return (
    <EngineCanvas camera={{ position: [0, 2, 4] }}>
      <Environment preset="night" />
      <Particles preset="fire" count={300} origin={[0, 0.1, 0]} />
      <Particles preset="smoke" count={100} origin={[0, 0.9, 0]} />
      <Particles ref={sparks} preset="sparks" count={300} emitting={false} ground={{ y: 0, bounce: 0.45 }} />
      <pointLight position={[0, 0.6, 0]} color="#FFB454" intensity={4} distance={6} />
    </EngineCanvas>
  );
};
```

These components must be inside `EngineCanvas`: they read the renderer and its device.

## `Particles`

Particles are camera-facing soft discs (or streaks), drawn as instanced sprites from two storage attributes the compute shader writes. One draw call per `<Particles>`.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `preset` | `'fire'`, `'sparks'`, `'smoke'`, `'magic'`, `'rain'`, `'snow'`, `'dust'`, `'puff'`, `'vortex'` | `'fire'` | The emitter to start from |
| `count` | `number` | `256` | How many live at once |
| `origin` | `[x, y, z]` | `[0, 0, 0]` | Where they spawn, world space |
| `follow` | `RefObject<Object3D>` | none | Spawn at this object's world position instead (a car's exhaust, a glow around the player) |
| `emitting` | `boolean` | `true` | `false` stops new particles; the live ones finish and the system stays warm |
| `config` | `EmitterConfig` | none | A whole emitter instead of a preset plus overrides |
| `timeScale` | `number` | `1` | Simulation speed |
| any `EmitterConfig` field | | from preset | Overrides, below |

`EmitterConfig` fields (sizes in metres, speeds in m/s):

| Field | What it is |
| --- | --- |
| `spread` | Half size of the spawn box around the origin, `[x, y, z]` |
| `velocity`, `jitter` | Initial velocity and its random share |
| `gravity` | `[x, y, z]` acceleration |
| `drag` | Velocity damping per second (0 none, 1 strong) |
| `swirl` | Sideways swirl around the emitter's up axis |
| `life`, `lifeJitter` | Seconds a particle lives, and the random share |
| `sizeStart`, `sizeEnd` | Size over life |
| `stretch` | Height over width (rain streaks, sparks) |
| `colorA`, `colorB` | Colour over life |
| `additive` | Additive (glows) or normal blending (smoke, rain) |
| `opacity` | Peak opacity |
| `ground` | `{ y, bounce?, friction? }`: a floor at height `y`. `bounce` above 0 bounces (sparks); 0 lands them as fading dots (rain splashes) |
| `attractor` | `{ at?, strength?, vortex? }`: pull toward a point (default the origin; negative pushes) and spin around it |

The ref is a `ParticlesHandle` with `burst(count, at?)`: respawn `count` particles this frame, from `at` when given. An `emitting={false}` system plus `burst` is a burst-only effect (hits, crashes, coins). `puff` is made for that: kicked-up dust on landings and footsteps. `PARTICLE_PRESETS` holds every preset's values.

```tsx
<Particles preset="sparks" count={200} follow={carRef} emitting={boosting} />
<Particles preset="rain" count={2000} origin={[0, 8, 0]} spread={[12, 0.5, 12]} ground={{ y: 0 }} />
<Particles preset="vortex" count={80} origin={[3, 1.1, 2]} emitting={!taken} />
```

## `Trail`

A ribbon behind a moving object (a light wall behind a bike, a skid line, a sword's arc), thinning and fading toward the tail. One mesh, a few dozen points updated on the CPU each frame.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `follow` | `RefObject<Object3D>` | required | What leaves the trail |
| `length` | `number` | `48` | Points kept |
| `width` | `number` | `0.3` | Width at the head, metres |
| `color` | `string` | `'#FF4FD8'` | |
| `orientation` | `'vertical'`, `'flat'`, `'camera'` | `'vertical'` | A wall, a line on the ground, or a ribbon that faces the camera |
| `offsetY` | `number` | `0` | Lift above the follow point |
| `taper` | `boolean` | `true` | Thin toward the tail |
| `additive` | `boolean` | `true` | Glow or plain |
| `minDistance` | `number` | `0.08` | A new point only after moving this far |
| `opacity` | `number` | `0.85` | |

## `Grass` and `LivelyGround`

A meadow: thousands of blades swaying in a wind that travels across the field and parting around whoever walks through. A compute shader bends every blade each frame; one draw call for the blades. `LivelyGround` is the ground under it: soft colour patches, a fine speckle and a slow shimmer.

```tsx
import React, { useRef } from 'react';
import { Grass, LivelyGround } from '@/engine';

export const Meadow = ({ player }: { player: React.RefObject<{ x: number; z: number }> }) => (
  <>
    <LivelyGround w={60} d={60} base="#5A9A3A" light="#6DAE44" />
    <Grass
      area={{ w: 60, d: 60 }}
      count={6000}
      height={0.55}
      colorA="#5A9A2C"
      colorB="#E6C24E"
      trample={() => (player.current ? { x: player.current.x, z: player.current.z } : null)}
    />
  </>
);
```

| `Grass` prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `area` | `{ x?, z?, w, d }` | required | The rectangle the blades cover, centre and size in metres |
| `count` | `number` | `3000` | Blades: 1,000 for a yard, 3,000 to 6,000 for a field |
| `height` | `number` | `0.5` | Blade height |
| `width` | `number` | `0.09` | Blade width at the root |
| `colorA`, `colorB` | `string` | `'#4E9A3C'`, `'#C9C35A'` | Body and tip colours |
| `follow` | `() => ({ x, z })` | none | Follow mode: blades fill a `tile` square around this point and wrap as it moves, so a big field stays dense for the cost of one tile. `area` and `mask` are then ignored |
| `tile` | `number` | `24` | The follow tile's width |
| `clearings` | `number` | `0.35` | How much ground is open between tufts (0 to 1) |
| `wind` | `number` | `1` | 0 still, 1 breezy, 2 stormy |
| `windDir` | `[x, z]` | `[1, 0.3]` | Wind direction |
| `mask` | `(x, z) => boolean` | none | Grow only where this returns true (not on paths or water) |
| `maskKey` | `string` | `''` | Change it to re-scatter after the mask changes |
| `trample` | `() => ({ x, z }) or null` | none | Blades lean away from this point |
| `trampleRadius` | `number` | `1.3` | |
| `seed` | `number` | `3` | Scatter seed |
| `receiveShadow` | `boolean` | `true` | |

| `LivelyGround` prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `x`, `z` | `number` | `0` | Centre |
| `w`, `d` | `number` | required | Size in metres |
| `base` | `string` | required | The main green |
| `light` | `string` | a lighter `base` | The second green the patches drift to |
| `y` | `number` | `0.004` | Height of the plane, a hair above a plain ground |
| `receiveShadow` | `boolean` | `true` | |

## `Water`

Rivers, moats, ponds and sea: one plane with a stylised TSL material (drifting deep and shallow patches, toon ripple lines, sparkles, a foam band at the edges) and, when `ripples` is on, a 2D wave simulation in a TypeGPU compute shader (raindrops, splashes, a wake). No reflection or refraction pass, so it stays cheap on a phone. It lives in `src/engine/render/Water.tsx` and is exported from `@/engine`.

```tsx
import React, { useRef } from 'react';
import { Water, type WaterHandle } from '@/engine';

export const Pond = () => {
  const water = useRef<WaterHandle>(null);
  // something fell in: water.current?.splash(2, -1);
  return <Water ref={water} x={0} z={0} w={12} d={8} color="#2F7FA8" ripples drops={3} />;
};
```

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `x`, `z` | `number` | `0` | Centre |
| `w`, `d` | `number` | required | Size in metres |
| `y` | `number` | `0.02` | Surface height |
| `color` | `string` | required | The water's colour; the other tones are derived from it |
| `deep`, `shallow`, `foam` | `string` | derived | Override a derived tone |
| `speed` | `number` | `1` | Drift speed of the patches and lines |
| `scale` | `number` | `1` | Size of the patches and lines |
| `waves` | `number` | `0.04` | Slow heave height, metres |
| `foamWidth` | `number` | `0.06` | Foam band as a fraction of the shorter side (0 none) |
| `opacity` | `number` | `0.94` | |
| `style` | `'toon'` or `'soft'` | `'toon'` | Hard ripple lines and sparkles, or smooth highlights |
| `ripples` | `boolean` | `false` | The ripple simulation (one compute pass a frame) |
| `drops` | `number` | `2` | Raindrop splashes per second, with `ripples` |
| `rippleHeight` | `number` | `0.12` | Metres |
| `disturb` | `() => ({ x, z, strength? }) or null` | none | A moving source each frame (a wading hero) |
| `simulate` | `boolean` | `true` | Debug: keep the ripple attribute but never run the compute shader |
| `receiveShadow` | `boolean` | `true` | |

The ref is a `WaterHandle` with `splash(x, z, strength?)`. The ripple grid is capped at 16,384 cells, so very large water gets coarser ripples.

## `GpuStrip`

A small HUD strip drawn by a TypeGPU fragment shader on its own WebGPU canvas (outside the 3D scene): moving stripes whose speed and glow follow `energy`. The racing HUD uses it as a reactive bar.

| Prop | Type | Default | What it does |
| --- | --- | --- | --- |
| `energy` | `SharedValue<number>` | required | 0 to 1, how energetic it animates |
| `colorA`, `colorB` | `[r, g, b]` (0 to 1) | violet, teal | The gradient |
| `style` | `ViewProps['style']` | height 28 | |
| `testID` | `string` | none | |

It wraps itself in `EngineErrorBoundary` and shows a flat bar if the GPU fails.

## Your own compute shader

For anything the presets do not cover (a flock, a cloth, a wave field), use the same split. The exports:

| Export | What it does |
| --- | --- |
| `useTypeGpuRoot()` | The TypeGPU root for the canvas's device (`null` before the renderer is ready). One per device, never destroyed: the device belongs to three |
| `attributeGpuBuffer(renderer, attribute)` | The `GPUBuffer` three created for a buffer attribute, or `null` until three has drawn with it once |
| `rendererDevice(renderer)` | The `GPUDevice` behind a `WebGPURenderer` |
| `typeGpuRootFor(device)` | The root for a device, created once |

The steps, as `Particles.tsx` does them:

1. Make a three `StorageInstancedBufferAttribute` (or `StorageBufferAttribute`, both from `three/webgpu`) and draw from it with a node material (`instancedBufferAttribute(attr)` in TSL).
2. In `useFrame`, poll `attributeGpuBuffer(renderer, attr)` until it is not `null`, then wrap it: `root.createBuffer(d.arrayOf(d.vec4f, n), gpuBuffer).$usage('storage').as('mutable')`.
3. Write the step with `tgpu.computeFn({ in: { gid: d.builtin.globalInvocationId }, workgroupSize: [64] })`, a function whose body starts with `'use gpu'`, and dispatch it each frame with `pipeline.dispatchWorkgroups(Math.ceil(n / 64))`. Any `useFrame` runs before three renders.

```ts
import tgpu, { d } from 'typegpu';
import * as std from 'typegpu/std';
import { attributeGpuBuffer, useTypeGpuRoot } from '@/engine';
```

The `'use gpu'` functions are compiled to WGSL at build time by `unplugin-typegpu/babel`, which the template's `babel.config.js` already loads.

## Gotchas

- Rules for `'use gpu'` code, or TypeGPU rejects the shader and the effect goes dark with an `[engine/gpu]` warning: vector maths goes through `typegpu/std` (`std.add`, `std.mul`, `std.mix`), scalars use plain operators; a `let` you reassign must hold a copy (`let pos = d.vec3f(buf.$[i].pos)`); guard the index against the count.
- `d.struct` field names must not be WGSL reserved words (`active`, `filter`, `type`, `target`, `self` and others). TypeGPU throws when the struct is built, at module load, so the whole game fails to open. Use names like `alive`, `mode`, `kind`.
- Never call `root.destroy()`: the device belongs to three. Destroy your own buffers on unmount.
- Budget: a few hundred particles for a local effect, 1,000 to 3,000 for weather. Each `<Particles>` is one draw call, so prefer one bigger system over many small ones.
- `<PostFX preset="glow" />` makes additive particles and emissive materials bloom ([Canvas and rendering](https://docs.moonspell.app/engine/canvas-and-rendering.md)).
- In development, particle systems add a line to the debug overlay ([Utilities](https://docs.moonspell.app/engine/utilities.md)).
