Experiment 07
Procedural Materials
Six surfaces built from noise, light, and a little shader math
Published: Aug 8, 2026
Updated: Sep 30, 2026
Experiment 07
Six surfaces built from noise, light, and a little shader math
Published: Aug 8, 2026
Updated: Sep 30, 2026
Open to new projects, ideas, and conversations
Experiment 07
Six surfaces built from noise, light, and a little shader math
Published: Aug 8, 2026
Updated: Sep 30, 2026
Most realistic materials are built from data captured from the real world: photogrammetry scans of actual surfaces, processed into texture maps for colour, roughness and height. I wanted to know whether a frontier AI model could skip that entirely and build realistic materials from mathematics alone, layering effects like bump and normal maps so a simple mesh reads as a complex surface.
None of the six materials use an image file. Every surface is generated from code, pixel by pixel, which is why the same material wraps a sphere, a cube or a torus knot without a seam.
Most of the patterns start from a combination of smooth random noise and a cellular pattern. For the cellular pattern, dots are scattered at random through space, one inside each cube of an invisible 3D grid. The grid only keeps the dots evenly spread and makes the nearest one quick to find; it never shows up in the result. Every spot on the surface belongs to whichever dot is nearest, which splits the surface into irregular cells, one per dot, with a border wherever two dots are equally close. The pattern's value is the distance to that nearest dot: zero on the dot, growing towards the cell's border.

These patterns get used in three ways: as the height of the surface (which sets the bumps and how light catches them), to blend between colours, and to decide where effects like a glow appear. Each material combines them differently:
This is the cellular pattern the gold and lava are built on:
float worley(vec3 p) { // p: a position in 3D vec3 i = floor(p); // which cube it's in vec3 f = fract(p); // where inside that cube float d = 1.5; // nearest distance found so far // Check this cube and the 26 around it (3 x 3 x 3) for (int x = -1; x <= 1; x++) { for (int y = -1; y <= 1; y++) { for (int z = -1; z <= 1; z++) { vec3 g = vec3(float(x), float(y), float(z)); // hash33 gives each cube its random dot position d = min(d, length(g + hash33(i + g) - f)); } } } return d;}Every material started out bump-mapped. Bump mapping doesn't change the shape, only how light hits it: wherever the height pattern slopes, the surface is lit as if it were tilted that way, so a flat face looks dented. The height pattern is computed from the same noise and cellular patterns above, not stored as an image. The code below finds that slope (the gradient of the height pattern) at every pixel.
vec3 bumped(vec3 N, vec3 position, float height, float amplitude) { // Ease off as displacement takes over the large shapes amplitude *= 1.0 - 0.40 * uDisplace; vec3 dpdx = dFdx(position); vec3 dpdy = dFdy(position); float dhdx = dFdx(height); float dhdy = dFdy(height); vec3 r1 = cross(dpdy, N); vec3 r2 = cross(N, dpdx); float det = dot(dpdx, r1); if (abs(det) < 1e-12) return N; vec3 gradient = sign(det) * (dhdx * r1 + dhdy * r2); return normalize(abs(det) * N - amplitude * gradient);}The toggle row on each card shows these layers separately: the height pattern, the tilted surface it produces, and the light layers stacked on top.

However, bump maps have a shortcoming: since the shape never changes, the outline stays smooth. No matter how deeply pitted a cube looked, its edges stayed perfectly straight, so I pushed for displacement: the same height pattern now moves the mesh's vertices in and out. That bought three things bumps can't:
Bumps didn't go away. The mesh can only move where it has vertices, so the finest detail (the weave's filaments, the gold's orange-peel texture) is still bumps, and the bumps ease off as displacement rises so the large shapes aren't counted twice. Every card has a slider for it, like dent depth on the gold or fissure depth on the lava, running from bumps only to fully displaced.
Turning displacement on tore the cube open along every edge. A cube stores each corner vertex once for every face that meets there, and each copy carries its own face's outward direction. Pushed along different directions, the copies pulled apart and left gaps. The fix averages the directions of every copy that shares a position, so all of them move the same way and the edges stay sealed. The shared direction also feeds the height pattern, since the carbon weave's height depends on which way the surface faces.



Frontier models can already build convincing real-world materials from nothing but mathematics. What they still miss is what a person sees at a glance: the straight edges and torn seams here were obvious the moment I looked, and only got fixed once I pointed them out. The mathematics came from the model; getting the result right took clear direction and a few rounds of iteration.