Experiment 09
Procedural Materials
Six surfaces built from noise, light, and a little shader math
Published: Aug 8, 2026
Experiment 09
Six surfaces built from noise, light, and a little shader math
Published: Aug 8, 2026
Open to new projects, ideas, and conversations
Experiment 09
Six surfaces built from noise, light, and a little shader math
Published: Aug 8, 2026
Every card renders the same compact Three.js scene, camera and geometry, leaving the surface shader as the only variable. Warped band families braid into the marble's veins; a cellular edge network cracks the lava's basalt crust into plates; warped cellular noise dents the hammered gold; two-surface refraction with a per-wavelength index bends the glass; thin-film interference over a warped platelet field colours the pearl; and a 2/2 twill, projected on three planes at once, weaves the carbon fibre. None of the shapes carry usable UVs, so all of that surface detail is projected triplanar and its normals are rebuilt from screen-space derivatives rather than sampled from a texture. The same height fields also run in the vertex stage, where they move the mesh itself. A small procedural studio — a graded sky, a soft key light, a stretched strip reflection and a lit backdrop behind the subject — stands in for an environment map, since that is what the glass, gold and clear coat actually reflect and refract. The controls write directly to shader uniforms, while the shared shape selector swaps only the underlying geometry. Lava keeps its colour values in HDR for a restrained bloom pass before the final image is tone-mapped.
The page was built by two different models, and the split is legible in the results. The first few passes — the layout, the card and slider scaffolding, and the initial version of all six shaders — were done by GPT 5.6 Sol on High. It was fast: the whole thing existed within a handful of minutes. It was also the weaker work. The layout it produced has survived essentially unchanged, but four of the six materials read as flat or plastic, and the reasons were structural rather than cosmetic — normals that never moved, patterns that were functions of fewer coordinates than the surface had, a heat threshold on the wrong side of its own noise field.
The rework of all six materials was done by Opus 5 on High, and the trade ran the other way. It came in two prompts — four materials, then the remaining two — but it was one effort with one method, and the section below treats it as such. Each pass took roughly half an hour rather than minutes, and most of that time went into looking: rendering the shader offscreen, reading the result, changing one term, rendering again. The difference in the output is less about knowing more physics than about being willing to spend twenty iterations finding out that the marble's problem was its polish rather than its veins.
The six failures looked unrelated and mostly were not. Four of them — glass, gold, pearl and marble — shared one shape: plausible colour math sitting on a normal that never moved, so “hammering” a metal or “frosting” a lens only tinted a perfectly smooth sphere. Three of them finished a light contribution by adding a reflection on top instead of layering it, which looks fine head-on and washes the silhouette to white. The two outliers were structural in their own way: the carbon weave was a function of two coordinates on a three-dimensional surface, and the lava thresholded its heat against the wrong side of its own noise field.


The weave started as a flat 2D pattern in local x and y, so it was effectively extruded along z: two faces of the cube showed the grid and the other four showed its cross-section stretched into plain stripes, which is what made it look broken on anything but a single lucky angle.


The fix samples the twill three times, once per axis-aligned plane, and blends the three by how squarely the local normal faces each one — the standard triplanar trick for painting detail onto a mesh that was never unwrapped. The twill itself changed too: it had been a per-cell coin flip between two tow directions, which reset at every cell edge and read as loose blocks rather than cloth. It is now a continuous cosine undulation with a four-cell period, which is the actual structure of a 2/2 weave — each tow floats over two cells and dips under two, shifting one cell per row.
Gold now bumps a warped Worley cell field so the dents actually catch and lose the light; glass frosts the same way, and separately refracts twice — once entering the body, once leaving it — so it bends and disperses light the way a real lens does instead of just tinting whatever sat directly behind it. Pearl swaps a view-angle colour ramp for real thin-film interference: the optical path length through a warped platelet field, one cosine per light wavelength, faded back to the film's mean colour once a fringe gets narrower than a pixel so it doesn't alias into static.
These two were held back from the first prompt and came out of it as the weakest surfaces on the page. Lava was not a rock at all: its heat threshold sat below the mean of the noise field it was thresholding, so nearly every pixel qualified as molten and the result was a ball of burning gas. The whole coordinate space also scrolled with time, which slid the rock across itself. It is now a static jigsaw of plates — the seam network is the difference between the two nearest cellular feature points, which goes to zero exactly on a boundary between cells — with a vesicular, bump-mapped crust, and every animated term moved into the heat instead of the geometry. Only some stretches of the network vent, gated by a slow mask, because a rock with every seam lit reads as a wireframe.
Marble was flat white with three near-black lines drawn on it. Its veins are now three band families braided by a shared domain warp, each a narrow dark core inside a much wider bleed, with ragged edges and a width that pinches and swells along their length. The larger change is underneath: calcite is translucent for a couple of millimetres, so the body now scatters light rather than stopping it at the surface, and the vein fill — being denser — blocks that scatter, which is what sinks the seams into the block instead of printing them on it.


The carbon fibre had a subtler version of the same problem, and it only showed at glare angles: the weave lit its tows individually and correctly, and then a uniform silver sheen was laid over the whole thing and contradicted all of it. Two mistakes were stacked there. The coat reflected along the raw geometric normal, so every tow across a face got an identical highlight; and it was added on top of the weave rather than composited with it, so near the silhouette — where the Fresnel term approaches one — it simply added white until the cloth disappeared underneath.
A real clear coat is thin and conformal. It is smooth compared with the filaments, but it still drapes over the tows, so it now reflects along a damped copy of the weave normal. That is enough to break the highlight: at the coat's roughness the specular lobe is about two degrees wide and the weave tilts the surface by four or five, so one blob becomes a field of per-tow glints. The second half of the fix is compositing rather than adding — the weave underneath is attenuated by exactly what the coat reflects, which is what a layer over something means.


The same audit ran across the other four. Marble had the identical fault and it was doing more damage there than anywhere: its polish was an additive sheen over the entire lit hemisphere, and pulling it into a proper coat is most of why the stone reads as stone now. Pearl had a mild case at the rim, where the film's Fresnel flare was washing the nacre out to plain white as the surface turned away. Gold and glass were already clean — gold multiplies the environment by a tinted Fresnel term rather than adding it, and glass mixes transmission against reflection, so both were energy-conserving by construction.
Every card can now show the pieces it is built from. Each material fills the same five slots — the finished composite, the height field driving its relief, the normal that height produces, and the two light contributions that get layered to make the composite — so the toggle row means the same thing on all six. The parts come out of one evaluation of the real material rather than a separate debug shader, which matters: a parallel debug path is free to drift from what the material actually does, and then it is explaining something that no longer exists.
It earns its keep as a debugging tool as much as an explanation. The marble was reworked twice from the composite before its layer view made the problem obvious in one glance — the body already looked like stone and the polish was washing it out — and the carbon clear-coat fix is far easier to believe when you can look at the coat on its own and watch it follow the weave.

The badge on each card records prompt-driven revisions to that material itself: its shader, parameters, defaults or surface treatment. Layout, renderer, scenery, controls and documentation changes do not count. A maintenance note beside the material definitions asks future agents to increment only the demos a material change actually touches.
Everything above is about getting the six surfaces to look right. What follows came afterwards and is a different kind of change: it moves the geometry, opens up what the controls reach, and puts each demo back together as one object.
Every material was bump-mapped, which perturbs the normal the lighting reads and nothing else. It is a convincing illusion right up to the silhouette, where it fails completely: a cube edge stayed a perfectly straight line no matter how deeply pitted the face beside it looked, because the rasteriser was still drawing the original undisplaced mesh. The fix is to move the vertices. Each material now exposes a coarse version of its height field to the vertex stage, which pushes each vertex along its normal and then rebuilds that normal from the field's gradient across an improvised tangent frame — there are no UVs here to differentiate against.


Two things had to be decided rather than derived. The step used for that gradient is deliberately close to the vertex spacing rather than infinitesimal: the surface only carries relief its tessellation actually resolved, and sampling finer would light detail the geometry does not have. And because the vertex stage now resolves the coarse half of the same field, the fragment bump backs off proportionally as the slider comes up — otherwise both stages apply the macro relief and it reads twice as deep.
The one shape that resists this is the icosahedron, and for a reason worth keeping: it is flat-shaded and non-indexed, so each corner carries a different normal in each of the five faces meeting there. Pushing those copies along their own normals pulls them apart and tears the solid open. It displaces along the radial direction instead, which every copy of a corner agrees on. It also has twelve unique corners, which is roughly the floor of what a height field can be carried by — a useful reminder that displacement is limited by the mesh, not by the shader.
Each material had three sliders because three was the number the scaffolding was built with, not because three parameters described it. Re-deriving them from what each shader actually does put the count at five, and the additions are mostly constants that were doing interesting work in silence: glass's dispersion was pinned at two percent, marble's translucency — the single term separating stone from white plastic — was a literal in the middle of an expression, and pearl's film thickness sets which interference order the shell sits in, so it picks the hue family outright. Gold's “Hammering” had been conflating how big each blow is with how hard it landed; those are now two controls.
Carbon's new one is deliberately a trap door. “Coat conformity” is how closely the clear coat drapes over the tows, and dragging it to zero reproduces the silver-blanket bug described above exactly — the coat flattens into a sheet and the weave disappears under it. A demo that can only show the fixed version is asking to be taken on faith.
The controls used to sit in a separate card below the render they drive. That was survivable while they were the only interactive part, but once the layer toggles gave each card its own control row the split read as two unrelated objects, so the sliders moved inside and a rule divides them from the description. The reset button had a smaller version of the same inconsistency: it eased the slider values home over about two thirds of a second and then snapped the body's rotation back in a single frame. It now interpolates both, taking whichever way round is shorter — the demos spin continuously, so the resting pose is hundreds of radians behind by the time anyone presses it.
“It only looks right from one side” is a specific, diagnosable symptom, not a vague quality problem: it means the pattern is a function of fewer coordinates than the surface has, so whichever axis got dropped shows up as a seam or an extrusion. On a mesh with real UVs that is a wrapping bug in the unwrap; on a procedural shape with no UVs at all — every shape in this demo — it means the noise function needs a third input, and triplanar projection is the standard way to supply one without authoring a UV set for four different topologies at once.
Every one of the flat-looking materials had the same shape: plausible colour math sitting on a normal that never moved. A micro-surface parameter that only scales a tint or a specular exponent can look reasonable in isolation and still leave the body reading as painted plastic, because the eye is more sensitive to where light bends than to what colour it lands as. Perturbing the normal first — even cheaply, even before the colour work is tuned — is what makes a surface look like it has a physical microstructure at all.
The screen-space-derivative bump technique used across all six materials takes an amplitude in world units, not an arbitrary strength: the pixel-footprint terms in its gradient cancel out, so a value that is not scaled against the feature size it is perturbing either does nothing or shreds the surface into noise. Every call site here divides the amplitude by its own cell size — the weave scale for carbon, the Worley cell scale for gold — specifically so that changing “how fine” a pattern is does not also silently change “how deep” it reads.
Three of the six materials finished a light contribution with color += reflection, and all three looked fine from the front, because a Fresnel term is small near the normal and the error is small with it. The failure is at the edges: as the surface turns away the term climbs towards one and keeps adding, so the silhouette washes out to plain white and takes every bit of surface detail with it. Layering is the whole fix — attenuate what is underneath by exactly what the top layer reflects — and it costs one multiply. The reason it kept getting missed is that the resting pose of every one of these demos points the interesting part of the surface straight at the camera.
It is easy to treat bump and displacement as the same effect at different costs. They are not: one perturbs the normal the lighting reads, the other moves the vertex the rasteriser draws, and only the second one exists at the silhouette. That gives the failure a precise signature — surface detail that looks convincing across a face and then dies at the outline, leaving a shape whose edges belong to a different, smoother object. Worth knowing that the usual instinct is backwards, too: the vertex work is the cheap half here by an order of magnitude, because it runs per vertex against a fragment stage that runs the same noise per pixel.
Re-deriving the controls from what the shaders actually do turned up several literals sitting in the middle of expressions and quietly setting the character of a material — the dispersion that splits the glass rim, the scattering depth that separates marble from plastic, the film thickness that picks which colour the nacre is. Each had been tuned once and then frozen, and none of them were visible as choices from the outside. Whether a number belongs on a slider is a decent test for whether it was ever really decided.
The layer toggle was added as a feature for readers and immediately paid for itself as a debugging tool. Two rounds of guessing at the marble from its composite changed the veins, which were not the problem; the first look at its layers showed a body that already read as stone under a polish layer that was erasing it. A composite is a sum, and a sum tells you almost nothing about which term is wrong — which is an argument for making the decomposition part of the thing itself rather than a throwaway debug shader, since a parallel debug path drifts out of sync with the material it is supposed to explain.
The in-app browser used to check most of this site's changes never fires requestAnimationFrame, so a Three.js scene never paints there — every screenshot on this page came from headless Chrome instead, rendering the exact shader module the page ships against a tile grid of materials, shapes and angles in one pass. That harness caught its own class of trap: a stray backtick inside a GLSL comment silently closed the JavaScript template literal holding the shader source, which renders as a blank black canvas with no error anywhere near the actual mistake.