---
name: blizzard-gusts
description: >
  Run a GPU blizzard in one Three.js file: 260,000 sprites driven by a
  compute kernel, two gust fronts on a drifting wind, birth upwind, no
  accumulation. Use when changing flake count, gusts, turbulence, or the
  sprite material on WebGPU. The reference file is snow.html beside this
  skill. If they disagree, the HTML wins.
---

# Blizzard gusts

One HTML file. No bundler, no npm, no textures. Three.js 0.186.1 from a CDN via an import map. `three` and `three/webgpu` both point at `three.webgpu.js`. `three/tsl` points at `three.tsl.js`, which imports `three/webgpu` and therefore the same module. One Three instance. Do not add a second import of `three.module.js`, and do not fall through to an unpinned `three/build/` URL.

`WebGPURenderer` only. Set `renderer._getFallback = null`. If `navigator.gpu` is missing, or the backend is not WebGPU after `init()`, stop and show the notice. Compute does not run on WebGL. No fallback scene.

Open it through a server. `file://` will not load the ES modules.

## Draw

- `Sprite` + `SpriteNodeMaterial`, `snow.count = COUNT` (260000).
- WebGPU points are 1 pixel. Do not switch this to `Points` or `PointsMaterial.size`.
- `frustumCulled = false`. Positions are in a storage buffer. The CPU never reads them back, so a frustum sphere would cull the whole field.
- `depthWrite: false`, `NormalBlending`, transparent. Do not use additive blending.
- Opacity is a soft disc from `uv()` times the per-flake alpha.
- `scaleNode` reads the size buffer. `positionNode` reads the position buffer. `colorNode` goes from blue-grey to white with speed.

## Buffers

Seven storage buffers, `COUNT` elements: position, velocity, seed, age, life, size, alpha. Prefer `instancedArray`. The `storage(StorageInstancedBufferAttribute)` branch is only there if `instancedArray` is missing.

No CPU readback. No `localStorage`. The HUD is derived from the uniforms, not from the buffers.

## Birth

`emitBirth(take, seed, dir, perp, inVolume)` is a plain JavaScript function. It inlines the node graph at each call site. Do not wrap it in `Fn()`. The TSL function signature has moved between versions, and a `Fn` here breaks the compute compile.

- `take` is 1 to write, 0 to leave the flake alone. Blend with `mix` / `step`. No `If()`.
- Most births are in the upwind band (`START`, `BACK`, `SPREAD`). `inVolume` or a random over 0.80 seeds anywhere in the field.
- Seed advances by the golden-ratio fraction so the next life is a different flake.
- `init` compute calls it with `take = 1` and `inVolume = 1`, ages scattered.
- `update` calls it at the end with `dead` as `take` and `inVolume = 0`.

Death: age past life, or distance past `KILL`, or height past `CEIL + 6`.

## Motion

- Wind direction and the two gust fronts are uniforms, written on the CPU.
- Fronts are Gaussians along the wind axis: position, width, gain. Recycle when they pass `+R`.
- `slam()` resets the second front (fast, wide, high gain) and sets `uPulse` to 2.4. `G` or a click with movement under 6 pixels.
- Turbulence is added to the target velocity, then velocity chases that target. Do not add turbulence to acceleration. That makes the flakes shiver in a line.
- Floor clamp at y = 0.06. No accumulation, no settled snow.
- Alpha tapers over the first 10% and last 20% of each flake's own life.
- `prefers-reduced-motion` slows the bearing and the calm wind. Keep that branch.

Fog is `Fog(0x070b12, 16, 68)`. It hides the spawn band and the kill radius. Widening it shows the pop.

## If it is slow

Fill rate is the limit. Halve `COUNT` before changing the kernel, the turbulence, or the gust math. `COUNT`, `FIELD`, `CEIL`, `START`, `BACK`, `SPREAD`, and `KILL` are tuned together. Changing one of them without the others leaves a hole or a visible spawn edge.
