Shader Lab
13.Procedural marble

Procedural marble

Arc 3 · Procedural Patterns · Lesson 13 of 35
coordinate signal color
0.70
5.0
4

Purpose

A sine wave of the coordinate gives parallel stripes; pushing turbulence inside the sine's argument tears those stripes into veins. That one move is the whole marble recipe.

Key insight

Marble isn't a new primitive. It's two old primitives arranged in a very particular order. A bare sin(x) painted across the canvas gives perfectly parallel stripes. If you add a noise value to the output of the sine, you get stripes with speckled noise on top — ugly, cheap. But if you add that same noise value to the input of the sine (to the thing the sine is reading), every band samples itself at a slightly different x than its neighbors. Bands wander, pinch, tear, reconnect. Veins.

The distortion lives in the coordinate layer, the sine lives in the signal layer, and the palette that turns sine output into "dark base and bright vein" lives in the color layer. Bands of sine, bent by turbulence, colored like stone.

Break it

1. Pull vein-distortion to 0. Pure parallel stripes. Teaches: the bones under marble are just sin. Marble is a sine wave you have to squint to recognize.

2. Crank vein-distortion to max. Bands dissolve into foggy noise with no direction. Teaches: enough distortion inside the argument and the sine can no longer hold its band structure. There is a specific amplitude past which "marble" stops reading as marble at all.

Direct Claude

"more veined / more stone" vein-distortion up "cleaner stripes / less veined" vein-distortion down "tighter grain / more veins across" grain up "wider grain / fewer veins" grain down "grittier veins / rougher edges" turbulence octaves up (in the coord warp) "smoother / softer veins" turbulence octaves down "higher contrast" color palette stops closer together
Meta-phrase you gain here: "distort in the coordinate layer, not the color layer" — a brief like "add some turbulence before the sine, not after" is specific and executable. It rules out a whole family of wrong implementations.
Combines with: Lesson 9 (FBM is the turbulence source), Lesson 14 (same recipe, different coordinate function), Lesson 15 (swap sine for Worley and you have cracked stone instead of veined stone).
the fragment shader running above
uniform float u_veinDistortion;  // coordinate layer: turbulence amplitude
uniform float u_grain;           // signal layer: sine frequency
uniform float u_octaves;         // turbulence roughness (used as int loop bound)

// hash + smooth value noise + fbm (4 octaves of turbulence)
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 < 8; i++) {
    if (float(i) >= u_octaves) break;
    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 — turbulence added inside the sine's argument
  float turb = fbm(p * 1.5 + u_time * 0.03);
  float x = p.x + u_veinDistortion * turb * 3.0;

  // SIGNAL LAYER — sine of the distorted coordinate
  float bands = 0.5 + 0.5 * sin(x * u_grain);

  // Sharpen the veins a touch so the bright bands read as veins, not stripes
  float vein = pow(bands, 2.2);

  // COLOR LAYER — two-tone stone palette
  vec3 base_dark  = vec3(0.18, 0.16, 0.22);
  vec3 base_mid   = vec3(0.42, 0.38, 0.46);
  vec3 vein_light = vec3(0.96, 0.93, 0.88);
  vec3 col = mix(base_dark, base_mid, smoothstep(0.0, 0.6, vein));
  col = mix(col, vein_light, smoothstep(0.6, 1.0, vein));

  gl_FragColor = vec4(col, 1.0);
}