Procedural wood
Purpose
Wood is marble with one change — replace x with distance-to-center — so the same sine-plus-turbulence recipe produces concentric rings instead of parallel veins.
Key insight
Two materials can share a recipe and differ only in what goes into it. Marble fed the sine a straight coordinate (p.x). Wood feeds the sine a radial coordinate (length(p - center), the distance from a chosen tree-center). A sine of a linear coordinate gives parallel bands. A sine of a radial coordinate gives concentric bands — rings.
The turbulence added to the sine's argument works exactly as in marble: it wobbles rings into natural irregular arcs instead of perfect bullseyes. Less turbulence than marble, though, because rings still need to read as rings. The coordinate layer is where the wood-vs-marble distinction lives; signal and color are almost identical.
Break it
1. Drag tree-center far off-canvas (either slider to ±3). Rings flatten into nearly-straight lines. Teaches: wood and marble are the same shader at different radii — at infinite distance from the ring-center, a circle is a straight line. This is the literal geometric bridge between marble and wood.
2. Crank ring-tightness to its minimum (2). Only one or two rings remain — you can see the underlying bullseye cleanly. Teaches: the radial coordinate itself is smooth; it's the sine that slices it into rings. Same coordinate, fewer slices, fewer rings.
Direct Claude
length primitive — the coordinate transform is lifted straight from there), Lesson 23 (triplanar projection puts 2D wood onto a 3D plank without UV authoring).
uniform float u_ringTightness; // signal layer: sine frequency of radial dist
uniform vec2 u_treeCenter; // coordinate layer: where the rings converge
uniform float u_turbulence; // coordinate layer: noise multiplier on rings
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float vnoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f*f*(3.0 - 2.0*f);
return mix(
mix(hash(i + vec2(0,0)), hash(i + vec2(1,0)), f.x),
mix(hash(i + vec2(0,1)), hash(i + vec2(1,1)), f.x),
f.y);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * vnoise(p);
p *= 2.0;
a *= 0.5;
}
return v;
}
void main() {
vec2 uv = (gl_FragCoord.xy - 0.5 * u_resolution) / min(u_resolution.x, u_resolution.y);
vec2 p = uv * 3.0;
// COORDINATE LAYER — marble used p.x; wood uses length(p - center)
float turb = fbm(p * 2.0 + u_time * 0.02);
float r = length(p - u_treeCenter) + u_turbulence * turb;
// SIGNAL LAYER — sine of the (slightly warped) radial distance
float rings = 0.5 + 0.5 * sin(r * u_ringTightness);
// shape the dark-ring / pale-wood contrast
rings = pow(rings, 1.6);
// COLOR LAYER — warm wood palette
vec3 pale = vec3(0.82, 0.58, 0.36);
vec3 mid = vec3(0.52, 0.30, 0.15);
vec3 dark = vec3(0.22, 0.11, 0.06);
vec3 col = mix(dark, mid, smoothstep(0.0, 0.5, rings));
col = mix(col, pale, smoothstep(0.55, 1.0, rings));
gl_FragColor = vec4(col, 1.0);
}