There is no model file on this page. No mesh, no texture, no .glb, no HDR environment map. The object you orbit is a mathematical function evaluated per pixel, and the whole thing — renderer, distance field, post chain — arrives in a 14.9 KB HTML document.
Live demo: https://smirnov-artur.github.io/webgl/lattice/
Open the network panel before you judge that claim. Four requests, 64.5 KB over the wire, and 49 KB of that is two web fonts — the typography outweighs the renderer three to one. The counter in the corner of the page reads GEOMETRY 0 B because it is telling the truth.
What the object actually is
A graded gyroid: a triply-periodic minimal surface, the structure used in additive manufacturing when you want stiffness or heat transfer without mass. The entire solid is five lines of signed distance field:
float g = dot(sin(p * k), cos(p.yzx * k)); // gyroid, TPMS
float d = (abs(g) - wall) / (k * 1.7); // shell around the surface
d = max(d, length(p) - 1.0); // outer skin
d = max(d, 0.30 - length(p)); // inner cavity
d = max(d, min(dot(p, A), dot(p, B))); // section wedge subtracted
k is a function of radius, not a constant. That is the "graded" part: the cell gets finer toward the core, the way a real printed lattice is graded where the loads concentrate.
The section cut is the part people ask about. It is two half-spaces subtracted from the field — which is why the cut face shows a genuine cross-section through the lattice instead of a hollow capped shell. Drag the SECTION slider and you are re-cutting a solid, not playing back an animation of a pre-made mesh. There is no mesh to make.
The parts that took the longest
An analytic entry test. Every ray gets a ray/bounding-sphere intersection first. Pixels that miss the specimen cost two dot products instead of a full march. On a shot where the object covers a third of the frame, that is most of your pixels bought for nothing.
Silhouette coverage out of failure. Grazing rays run out of march steps before they converge — that is what makes ray-marched edges crunchy and full of holes. Instead of throwing those rays away, I keep their closest approach to the surface and reuse it as partial coverage. The rim fills in and the edge antialiases, and it costs nothing extra because the number was already computed and discarded.
No HDR file. The environment is a two-stop sky plus a warm softbox and a cool rim, each a broad cosine lobe with a tight core. Roughness widens the lobe instead of sampling a blurrier mip, so the whole image-based lighting is about a dozen instructions and zero bytes downloaded. This is the single biggest reason the page weighs what it does: an HDR environment is usually the heaviest asset in a scene like this, and here it does not exist.
Metal BRDF with roughness-aware Fresnel, per-channel dispersion (the reflected ray is fanned around the view tangent, wider at grazing angles), and heat-tempering colours — straw into blue — graded by depth into the core. SDF soft shadows and AO, skipped entirely on faces turned away from the key light. A volumetric core accumulated in front of the first hit, so the glow only leaks out through open cells and the section cut.
The post chain is hand-written too: bright-pass with a soft knee, two levels of separable Gaussian, then a composite doing ACES (Hill fit), edge-weighted chromatic aberration, unsharp, vignette, grain and dither, converting to linear Display-P3 where the display supports it.
The performance trick worth stealing
Quality here is expressed as ray-march pixels per CSS pixel, not as a fixed resolution. A 1× laptop should not be undersampled and a 3× phone should not be melted, and those are the same setting expressed correctly.
The adaptive scaler measures frame time against an estimate of the refresh period rather than against a fixed 16.7 ms. This matters more than it sounds. If you compare against 16.7 ms on a vsynced display, every frame looks marginally "slow", the scaler ratchets down, and it can never climb back — you end up at half resolution on hardware that had headroom the whole time.
Context is requested with alpha:false, antialias:false, depth:false, stencil:false, powerPreference:high-performance, and a first attempt at failIfMajorPerformanceCaveat:true. A null answer there means the browser would fall back to a software rasteriser — so the low tier gets selected up front instead of after the user watches five seconds of slideshow.
Measured, not estimated
Desktop, RTX 3050, hardware ANGLE/D3D11:
| value | |
|---|---|
| FPS | 60 (vsync ceiling) |
| page weight over the wire | 64.5 KB across 4 requests |
| of which, two web fonts | 49 KB |
| the document carrying the entire renderer | 14.9 KB |
| geometry + textures | 0 B |
The march resolution sits around 0.9× CSS pixels on the laptop and about 0.5× device pixels on a 390×844 phone at DPR 3, adapting continuously.
One honest correction, because I got this wrong in my own notes first: an earlier version said 95 KB. That was the uncompressed total. Over the wire, gzipped, it is 64.5 KB. Both numbers were real measurements — they just measured different things, and I had not said which.
Why build it this way
Because "it's only 14.9 KB" is not the point — it is the receipt. A distance field is a parametric object. The three sliders on that page are not presets, they are terms in the equation, and the solid re-forms around them because there was never a fixed model to violate. That property is the whole reason I use SDFs for engineering work: when the client says "now make the wall thinner", nothing has to be re-exported.
Raw WebGL 2, no library, hand-written GLSL, one HTML file. Drag to orbit — the camera is on a critically damped spring rather than a linear follow, which is a small thing you feel immediately and never consciously notice.
If you build things like this, I would genuinely like to hear how you handle grazing-ray coverage — the closest-approach trick is the best I have found, but I doubt it is the best there is.
I take on WebGL work, mostly real-time product configurators and engineering visualisation. More at smirnov-artur.github.io/webgl, or Telegram @smirnovarturr.
Top comments (0)