260,000 flakes, and JavaScript never writes a position. A compute kernel does, every frame, from a wind direction and two gust fronts that travel through the field.
This is one HTML file on Three.js 0.186.1 and WebGPU. The CPU writes time, the bearing, and the fronts. I use AI to write a lot of the kernel. Where a gust should curl, and that the flakes skim the ground and never pile up, come from 25+ years of JavaScript, game development, modding and level design.
Sprites
WebGPU draws a point as a single pixel, so each flake is a sprite and snow.count is 260,000. PointsMaterial.size does nothing on this backend. frustumCulled stays false, because the positions live in a storage buffer the CPU never reads, and a bounding sphere built from that would be wrong. The sprite is a soft disc in the opacity node, and it blends normally. Additive blending turns a blizzard into a white lamp.
Birth and death
Most flakes appear in a band upwind of the field. A few are seeded through the volume, so the air stays full when the bearing swings. A life lasts a few seconds. Too old, too far from the origin, or above the ceiling, and the flake is reborn at the spawn line. emitBirth is a plain function that pastes the node graph in place. It runs once to fill the buffers, and again at the end of every step. A TSL function would need a signature that has moved between versions, so it stays JavaScript. The blend is a mix, with no branch in the shader.
Gusts
The bearing drifts, with two slow wobbles on top of the turn. Two fronts travel along that wind. Each one is a Gaussian: a position, a width, a gain. The meter in the corner is those gains, added, and then eased back after a slam. Turbulence is mixed into the velocity the flake is steering toward, which is why a gust carries it through an eddy. Put that push on the acceleration and the motion goes stiff. G, or a click that did not turn into a drag, slams the second front through the field.
On the ground
Height is clamped at 0.06. Nothing settles, and nothing accumulates. The alpha fades in over the first tenth of a flake's own life and fades out over the last fifth, so the field churns. Colour moves from blue-grey to white as speed rises. Fog starts at 16 and ends at 68, which hides the spawn band and the radius where flakes are killed.
One copy of Three
The page loads three.webgpu.js once. three and three/webgpu in the import map both point at that file, and three/tsl imports three/webgpu, so the node graph and the renderer are the same module. Compute does not exist on WebGL, so the renderer has no fallback. If the backend is not WebGPU, the page stops.
The frame is that file. The needle on the compass is the wind. The small mark is where the camera is looking. The numbers update about ten times a second, because faster than that they are unreadable.
WebGPU only, on Three.js 0.186.1, with no WebGL fallback. If the browser has no WebGPU, the page stops. Open it through a server, because file:// will not load the modules. php -S localhost:8000 or npx serve in the folder is enough. If the frame rate falls over, halve COUNT before you touch the kernel. Fill rate is the limit, not the simulation.
- Open it on its own
snow.html, the project-
SKILL.md
If you add a third gust, give it the same shape as the other two: a position along the wind, a width, a gain, recycled when it leaves the field. And leave the floor clamp where it is. A flake that is allowed to settle will want a pile, and this kernel does not build one.