Shader Lab
14.Procedural wood

Procedural wood

Arc 3 · Procedural Patterns · Lesson 14 of 35
coordinate signal color
9.0
0.00
0.00
0.35

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

"tighter grain / denser rings" ring-tightness up "wider grain / looser rings" ring-tightness down "cross-section / bullseye in the middle" tree-center inside the frame "sawn plank / straight grain" tree-center far off one edge "more natural / wavier rings" turbulence amount up (kept small) "cleaner / more diagrammatic rings" turbulence amount down toward zero "warmer wood / cooler wood" color palette shift
Meta-phrase you gain here: "same recipe, different coordinate" — recognizing that marble and wood are one shader with p.x vs. length(p - c) swapped is the template you'll reuse for every procedural material after this.
Combines with: Lesson 13 (its sibling — same recipe, different coord), Lesson 3 (length primitive — the coordinate transform is lifted straight from there), Lesson 23 (triplanar projection puts 2D wood onto a 3D plank without UV authoring).
the fragment shader running above
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);
}