Skip to main content
GameDev.net gamedev.net

Procedural Marble

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Marble is the classic demonstration that a procedural material is usually a simple pattern plus a distortion. Start with parallel bands from a sine wave, push the wave along with layered noise, then reshape the result so the bands become thin veins in stone. Every step is one line, and each is exposed as a control.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image6
1 uniform float uVeinScale; // @param 1.0..12.0 = 4.0 "Vein scale"
2 uniform float uDistortion; // @param 0.0..3.0 = 1.5 "Vein distortion"
3 uniform float uSharpness; // @param 0.5..8.0 = 3.0 "Vein sharpness"
4 uniform vec3 uStoneColor; // @param 0.0..1.0 = 0.88, 0.87, 0.83 color "Stone"
5 uniform vec3 uVeinColor; // @param 0.0..1.0 = 0.14, 0.18, 0.24 color "Vein"
6
7 float hash21(vec2 cell) {
8 return fract(sin(dot(cell, vec2(12.9898, 78.233))) * 43758.5453);
9 }
10
11 float valueNoise(vec2 p) {
12 vec2 cell = floor(p);
13 vec2 local = fract(p);
14 vec2 weight = local * local * (3.0 - 2.0 * local);
15 float bottom = mix(hash21(cell), hash21(cell + vec2(1.0, 0.0)), weight.x);
16 float top = mix(hash21(cell + vec2(0.0, 1.0)), hash21(cell + vec2(1.0, 1.0)), weight.x);
17 return mix(bottom, top, weight.y);
18 }
19
20 float fbm(vec2 p) {
21 float sum = 0.0;
22 float amplitude = 1.0;
23 float normalization = 0.0;
24 for (int i = 0; i < 6; i++) {
25 sum += amplitude * valueNoise(p);
26 normalization += amplitude;
27 amplitude *= 0.5;
28 p *= 2.0;
29 }
30 return sum / normalization;
31 }
32
33 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
34 vec2 uv = fragCoord / iResolution.xy;
35 uv.x *= iResolution.x / iResolution.y;
36 vec2 p = uv * uVeinScale;
37 float turbulence = fbm(p * 1.6) * 2.0 - 1.0;
38 float bands = sin((p.x + p.y * 0.35 + turbulence * uDistortion) * 3.1415927);
39 float veins = pow(1.0 - abs(bands), uSharpness);
40 float grain = 0.06 * (fbm(p * 9.0) - 0.5);
41 vec3 color = mix(uStoneColor, uVeinColor, veins) + grain;
42 fragColor = vec4(clamp(color, 0.0, 1.0), 1.0);
43 }
44

Learn from this shader

How it works

A sine of the diagonal coordinate produces regular stripes. Adding turbulence, recentred to run from minus one to one, to the sine argument bends them: where the noise is large the wave arrives early, where it is small it lags, and the stripes wander. The stripe value is then folded with an absolute value and raised to a power. The fold puts a peak wherever the wave crosses zero, and the exponent controls how fast that peak falls off, turning a soft band into a narrow vein. Faint high frequency noise is added as grain, because a perfectly clean gradient reads as plastic.

Try changing

Set Vein distortion to zero to recover the plain stripes the effect is built from. Raise Vein sharpness to squeeze the veins thin, and lower it for a hazy agate look. Pick Stone and Vein colours far apart in value to see the shaping clearly, or close together for a subtle material. In a fork, replace the diagonal argument with a radial one for a growth ring pattern.

Using it in a game

The output is a base colour or a mask, not a lit material, so drive roughness and normal detail from the same vein term when you port it. Because it is static, this is a good candidate for baking to a texture once instead of evaluating six noise octaves per pixel.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...