Raw WebGL2, no three.js — the lattice is a graded gyroid TPMS defined as a signed distance function, sphere-traced per pixel in a single fragment shader. One inline , 31.8 KB of source. No mesh, no model file anywhere.
The geometry submitted to the GPU is one full-screen triangle from gl_VertexID, no vertex buffer. Wrapped gl.drawArrays to count calls: 7 per frame — 1 for the SDF scene pass, 6 for post (bright-pass, a two-level separable Gaussian blur, then a composite doing ACES tonemap, per-channel chromatic aberration, vignette, grain).
Cell size grades radially off one smoothstep on the frequency term — dense at the core, open at the skin, no second field. The part that took the most iteration wasn’t the raymarch, it was getting the surface to read as metal instead of plastic: dispersion runs three slightly offset refraction paths through the same field, one per color channel.
Numbers, measured just now on this machine (RTX 3050, 1250x1276 canvas, DPR 1):
- 60 fps steady, vsync-capped
- ~65 KB total transferred for the page: 15 KB gzip for HTML+CSS+JS+shaders together, ~50 KB for two subsetted variable fonts, zero images, zero model files
- 3 quality tiers (56 / 80 / 88 raymarch steps for low/mid/high), picked automatically: first a probe for a software-rasterizer fallback, then touch + screen size
Where it breaks: antialiasing is off on purpose, leaning on DPR and the post pass instead — push DPR down and the lattice edges alias visibly. No WebGL2 falls back to a static panel; only tested that on evergreen browsers. Running a raymarcher flat-out at 60fps draws real power and I haven’t capped the frame rate or measured battery impact.
Happy to get into the distance field or the tiering logic if anyone’s curious.
