A sailboat under a low sun. The water is a shader, the clouds are four noise layers on the dome, and the reflection is a real second render pass. No textures and no model files — the whole scene is source.
Drag to orbit; scroll or pinch to move closer. Every camera move the pointer can make is also a button in the corner of the scene, so the view is reachable from a keyboard. The readout is the last half-second the renderer actually spent.
What is actually running
No textures and no model files
The sky, the clouds, the water and the foam are one fragment shader each. The hull and its deck are lofted from one table of nine stations, so the whole scene is code — nothing is fetched from a CDN and nothing is decoded at runtime. Even the planking on the deck is a pattern in the shader rather than an image.
The wave sum is declared once
The boat has to sit on the same surface the shader draws, so the four waves live in one array and are emitted twice: as GLSL for the vertex stage, and as the JavaScript the hull floats on. Two hand-written copies would drift the first time somebody tuned one of them.
The reflection is a real one
The scene is drawn a second time from a camera mirrored in the water plane, into a render target the water samples through its own wave normal. That is why the boat and the clouds move in the reflection, and why the ripples break it up instead of leaving it glass.
The normals are the slope, not a guess
Each vertex takes finite differences of the same wave sum to find its normal, so the lighting follows the actual surface rather than a second approximation of it. The sun that lights the water is the same vector that lights the boat.
The wake is computed, not painted
The boat holds a constant-radius turn, so the water shader can work out where the boat has just been from a single curvature term and bend the foam band to match. The term was checked against the integrated path before it went in: within 0.03 units at ten behind, five per cent at eighteen.
The clouds are four layers, not one
Four noise layers project onto the dome at four different footprints and composite back to front. Each is dimmed by the coverage already in front of it, which is what gives the stack a lit top and a shadowed base without marching a ray through it.
The colour space is converted by hand
Every shader here ends with the engine's own colour-space include, and it has to. Setting the renderer to sRGB output only makes it define the conversion helper; a hand-written shader is converted by that include and by nothing else. Leaving it out writes linear numbers into an sRGB buffer, and the scene sat roughly half as bright as its own palette for as long as nobody measured a pixel.
What it costs
three.js is fetched on this route and on no other. The shared shell every page downloads is unchanged by this scene, and that is measured rather than asserted — npm run measure prints the shell and this route as two separate numbers, and /quality/speed publishes both.
Two behaviours keep it from being rude about the cost. It renders nothing while it is scrolled out of view or while the tab is hidden, and a visitor who has asked for reduced motion gets one still frame instead of an animation.
Controls
Drag
orbit the camera around the boat
Scroll or pinch
change the distance
Quality
halves the pixel ratio and the mesh, for a slower machine
The other 3D work in this library is CSS: tilt, flip and orbit transforms that need no engine at all. They are in the gallery under 3D, and they cost a fraction of what this page does.
This site uses Google AdSense to show ads. It sets cookies and may collect browsing data. Learn more.