Procedural marble
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
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);
}