Wood & Marble
A block carved from two solids, one half wood and one half marble, turning over a floor of marble slabs. Neither texture is a picture wrapped round the block: each gives a colour to every point in space, so a plane that slices through the block finds the rings and veins carrying on inside. The floor's slabs are all the same slab, cut from marble whose noise repeats across it, so they meet with no seam.
· Noise & Texture · Fragment shader
Teaches Solid procedural textures, noise that tiles
Builds on 03 · Floating 3D Scene (rotation), 08 · Kiln Fire (3D hash), 17 · Kiln Floor Tiles (wrap into a period)
Watch
The idea
Most textures are pictures stuck on a surface, and that shows wherever two faces meet or the object is cut: the pattern stops at the edge. A solid texture works differently. It gives a colour to every point in space, worked out from the point alone, and an object is carved out of it the way a statue is carved out of stone. Every face of the block agrees with its neighbours at the edges, because they are all slices of the same solid, and a cut through the block shows what was inside.
Wood and marble are the classic solid textures, and both are built from 3D noise: wood from rings around a log, bent by noise, and marble from stripes pushed sideways by turbulence. The second idea is noise that repeats. Episode 17 wrapped its 2D noise lattice so the floor repeated with the picture; here the same wrap makes 3D noise repeat across a floor slab, so slabs that are all the same slab can be laid side by side with no seam.
It is built in four steps, and each step is a shader you can run on its own.
Stage 1 · A slice through a log
tnoise is value noise in three dimensions: episode 8's hash31 gives a random number to every corner of a lattice of cubes, and the eight corners round a point are blended smoothly (f * f * (3 - 2f) in each direction). Before a corner is hashed its whole numbers are wrapped by period, so the noise repeats every period cubes; the wood passes 1e4, a period nothing here comes near, so for now it never repeats. tfbm adds four octaves, each lattice twice as fine and half as strong.
wood is a log lying along z, its heart at (-1.3, -0.9), off to the side, and drifting a little along the log. The distance from the heart counts the rings: fract(r * rings) runs from 0 to 1 across each ring, and late, which rises from 0.55 and falls away just before 1, darkens the end of each ring the way the wood grown late in a summer is darker. Before the rings are counted, the distance is bent by tfbm (as far as warp says), so the rings wobble, and noise far finer across the log than along it adds the fine streaks of grain.
The picture is the plane z = time through the log, a slice across its end, and it slides slowly along: the rings creep and wobble as they would from one sawn end to the next.
stage-1-rings.metal
// Stage 1 · A slice through a log
// Wood as a solid: a colour for every point in space, worked out from the point alone. The picture is a flat slice
// across a log, its end grain, and the slice slides slowly along the log, so the rings creep and wobble as they would
// from one sawn end to the next.
// @param speed 0.5 0.0 2.0 How fast the slice slides through the log
// @param rings 9.0 3.0 20.0 How many tree rings to a unit of wood
// @param warp 0.5 0.0 1.0 How far noise bends the rings and the grain
// Episode 17's wrap: x into 0 to m, below 0 too.
float wrap(float x, float m) {
return x - m * floor(x / m);
}
// Episode 8's hash: a random number from 0 to 1 for each whole-numbered
// cube of space.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
// Value noise in 3D: a random value at every corner of a lattice of cubes,
// blended smoothly across each cube. The lattice wraps every `period` cubes
// (a whole number), so the noise repeats; a huge period never shows it.
float tnoise(float3 q, float period) {
float3 i = floor(q), f = fract(q);
f = f * f * (3.0 - 2.0 * f);
float3 a = float3(wrap(i.x, period), wrap(i.y, period),
wrap(i.z, period));
float3 b = float3(wrap(i.x + 1.0, period), wrap(i.y + 1.0, period),
wrap(i.z + 1.0, period));
float n000 = hash31(float3(a.x, a.y, a.z));
float n100 = hash31(float3(b.x, a.y, a.z));
float n010 = hash31(float3(a.x, b.y, a.z));
float n110 = hash31(float3(b.x, b.y, a.z));
float n001 = hash31(float3(a.x, a.y, b.z));
float n101 = hash31(float3(b.x, a.y, b.z));
float n011 = hash31(float3(a.x, b.y, b.z));
float n111 = hash31(float3(b.x, b.y, b.z));
float lo = mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y);
float hi = mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y);
return mix(lo, hi, f.z);
}
// Four octaves, each lattice twice as fine and its period twice as long,
// so it still repeats where the first one does. From 0 to 1.
float tfbm(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * tnoise(q, period);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// Wood: a log along z, its heart off to one side. The distance from the
// heart, bent by noise, counts the rings: pale growth darkening to its end.
float3 wood(float3 q, float rings, float warp) {
float3 earlywood = float3(0.780, 0.500, 0.280);
float3 latewood = float3(0.330, 0.150, 0.070);
float2 heart = float2(-1.30, -0.90) + 0.06 * q.z;
float bend = (tfbm(q * float3(1.6, 1.6, 0.5), 1e4) - 0.5) * warp * 0.9;
float r = length(q.xy - heart) + bend / rings * 4.0;
float ring = fract(r * rings);
float late = smoothstep(0.55, 0.92, ring)
* (1.0 - smoothstep(0.94, 1.0, ring));
// Fine grain: noise stretched along the log.
float grain = tnoise(q * float3(34.0, 34.0, 1.2), 1e4);
float3 col = mix(earlywood, latewood, late * 0.85 + grain * 0.25 * warp);
return col * (0.88 + 0.24 * tnoise(q * 3.0 + 7.0, 1e4));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
// The slice z = time, 2 units tall.
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 q = float3(s, u.time * p.speed * 0.4);
return float4(wood(q, p.rings, p.warp), 1.0);
}Stage 2 · Marble beside it
turbulence is tfbm with every octave folded at its middle: abs(noise * 2 - 1) is 0 where the noise crosses a half and rises on both sides, so each octave leaves a sharp crease. marble makes stripes from sin(v), where v slants through space (1.7x + 1.1y - 0.6z), and pushes them sideways by the turbulence times push, so the stripes bend and crease. The stripes shade from a warm grey cloud to a cream stone, and where the sine crosses zero (abs(s) < 0.08) runs a thin vein of gold. The right half of the slice is marble, the left half still wood, with a dark seam of glue between.
stage-2-marble.metal27 new lines since stage 1
// Stage 2 · Marble beside it
// Marble is a solid too: stripes from a sine of the position, pushed sideways by turbulence, noise folded at its
// middle so it creases. Where the sine crosses zero runs a thin vein of gold. The right half of the slice is marble,
// the left half still wood.
// @param speed 0.5 0.0 2.0 How fast the slice slides through the solids
// @param rings 9.0 3.0 20.0 How many tree rings to a unit of wood
// @param warp 0.5 0.0 1.0 How far noise bends the rings and the grain
// @param veins 0.5 0.0 1.0 How turbulent the marble's veins are
// Episode 17's wrap: x into 0 to m, below 0 too.
float wrap(float x, float m) {
return x - m * floor(x / m);
}
// Episode 8's hash: a random number from 0 to 1 for each whole-numbered
// cube of space.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
// Value noise in 3D: a random value at every corner of a lattice of cubes,
// blended smoothly across each cube. The lattice wraps every `period` cubes
// (a whole number), so the noise repeats; a huge period never shows it.
float tnoise(float3 q, float period) {
float3 i = floor(q), f = fract(q);
f = f * f * (3.0 - 2.0 * f);
float3 a = float3(wrap(i.x, period), wrap(i.y, period),
wrap(i.z, period));
float3 b = float3(wrap(i.x + 1.0, period), wrap(i.y + 1.0, period),
wrap(i.z + 1.0, period));
float n000 = hash31(float3(a.x, a.y, a.z));
float n100 = hash31(float3(b.x, a.y, a.z));
float n010 = hash31(float3(a.x, b.y, a.z));
float n110 = hash31(float3(b.x, b.y, a.z));
float n001 = hash31(float3(a.x, a.y, b.z));
float n101 = hash31(float3(b.x, a.y, b.z));
float n011 = hash31(float3(a.x, b.y, b.z));
float n111 = hash31(float3(b.x, b.y, b.z));
float lo = mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y);
float hi = mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y);
return mix(lo, hi, f.z);
}
// Four octaves, each lattice twice as fine and its period twice as long,
// so it still repeats where the first one does. From 0 to 1.
float tfbm(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * tnoise(q, period);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// Turbulence: octaves folded at their middle, each leaving a sharp crease.
// From 0 to about 1.
float turbulence(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * abs(tnoise(q, period) * 2.0 - 1.0);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// Wood: a log along z, its heart off to one side. The distance from the
// heart, bent by noise, counts the rings: pale growth darkening to its end.
float3 wood(float3 q, float rings, float warp) {
float3 earlywood = float3(0.780, 0.500, 0.280);
float3 latewood = float3(0.330, 0.150, 0.070);
float2 heart = float2(-1.30, -0.90) + 0.06 * q.z;
float bend = (tfbm(q * float3(1.6, 1.6, 0.5), 1e4) - 0.5) * warp * 0.9;
float r = length(q.xy - heart) + bend / rings * 4.0;
float ring = fract(r * rings);
float late = smoothstep(0.55, 0.92, ring)
* (1.0 - smoothstep(0.94, 1.0, ring));
// Fine grain: noise stretched along the log.
float grain = tnoise(q * float3(34.0, 34.0, 1.2), 1e4);
float3 col = mix(earlywood, latewood, late * 0.85 + grain * 0.25 * warp);
return col * (0.88 + 0.24 * tnoise(q * 3.0 + 7.0, 1e4));
}
// Marble: stripes from sin(v), pushed sideways by turbulence times `push`,
// with a thin gold vein where the sine crosses zero.
float3 marble(float3 q, float v, float push, float period) {
float3 stone = float3(0.960, 0.930, 0.860);
float3 cloud = float3(0.780, 0.700, 0.620);
float3 vein = float3(0.880, 0.480, 0.150);
float turb = turbulence(q, period);
float s = sin(v + turb * push);
float3 col = mix(cloud, stone, 0.3 + 0.7 * smoothstep(-0.4, 0.95, s));
float line = 1.0 - smoothstep(0.0, 0.08, abs(s));
col = mix(col, vein, line * 0.85);
return col;
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 q = float3(s, u.time * p.speed * 0.4);
// Marble where x is above 0: the stripes run on a slant through space.
float3 m = marble(q * 2.6, (q.x * 1.7 + q.y * 1.1 - q.z * 0.6) * 6.0,
1.0 + 5.0 * p.veins, 1e4);
float3 w = wood(q, p.rings, p.warp);
float3 col = q.x < 0.0 ? w : m;
// A dark seam of glue where they meet.
col *= mix(0.35, 1.0, smoothstep(0.004, 0.012, abs(q.x)));
return float4(col, 1.0);
}1 line from stage 1 that are gone
return float4(wood(q, p.rings, p.warp), 1.0);Stage 3 · Carve a block from it
Now the solids fill space and a block is cut from them. A box is the space between three pairs of planes, so hitBlock finds where a ray meets it without marching: for each pair, where the ray crosses the near plane and the far one. The ray is inside once it has passed every near plane and leaves at the first far one, and whichever near plane it passed last is the face it hit. The cut is one more plane, dot(q, n) = c: the ray must be on its near side too, so where the ray crosses it late enough, the cut is the face. One test, no steps, and the texture is worked out once, at the hit.
The block turns and is tipped, but the ray is turned into the block's own space (toBlock) before the test, so q, the point it hits, is the same point of wood or marble however the block turns: the texture turns with it rather than the block sliding through it. The block is wood where its own x is below 0 and marble above. The cut sweeps in and out (c from 1.15, which misses the block, down by cut × 1.55), and the face it leaves shows the rings and veins carrying on inside, meeting the outside faces at every edge.
stage-3-carved.metal63 new lines since stage 2
// Stage 3 · Carve a block from it
// Now the two solids fill space, and a block is cut out of them: wherever a ray from the camera meets the block, the
// colour is the solid's at that point. A plane slices off a corner, sweeping in and out, and the face it leaves shows
// the rings and the veins carrying on inside.
// @param speed 0.5 0.0 2.0 How fast the block turns and the cut sweeps
// @param rings 9.0 3.0 20.0 How many tree rings to a unit of wood
// @param warp 0.5 0.0 1.0 How far noise bends the rings and the grain
// @param veins 0.5 0.0 1.0 How turbulent the marble's veins are
// @param cut 0.6 0.0 1.0 How deep the slicing plane reaches into the block
constant float TAU = 6.2831853;
// Half the block's size.
constant float HALF = 0.62;
// Episode 3's rotation.
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// Episode 17's wrap: x into 0 to m, below 0 too.
float wrap(float x, float m) {
return x - m * floor(x / m);
}
// ---- Noise in three dimensions -----------------------------------------------------------------------------------
// Episode 8's hash: a random number from 0 to 1 for each whole-numbered
// cube of space.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
// Value noise in 3D: a random value at every corner of a lattice of cubes,
// blended smoothly across each cube. The lattice wraps every `period` cubes
// (a whole number), so the noise repeats; a huge period never shows it.
float tnoise(float3 q, float period) {
float3 i = floor(q), f = fract(q);
f = f * f * (3.0 - 2.0 * f);
float3 a = float3(wrap(i.x, period), wrap(i.y, period),
wrap(i.z, period));
float3 b = float3(wrap(i.x + 1.0, period), wrap(i.y + 1.0, period),
wrap(i.z + 1.0, period));
float n000 = hash31(float3(a.x, a.y, a.z));
float n100 = hash31(float3(b.x, a.y, a.z));
float n010 = hash31(float3(a.x, b.y, a.z));
float n110 = hash31(float3(b.x, b.y, a.z));
float n001 = hash31(float3(a.x, a.y, b.z));
float n101 = hash31(float3(b.x, a.y, b.z));
float n011 = hash31(float3(a.x, b.y, b.z));
float n111 = hash31(float3(b.x, b.y, b.z));
float lo = mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y);
float hi = mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y);
return mix(lo, hi, f.z);
}
// Four octaves, each lattice twice as fine and its period twice as long,
// so it still repeats where the first one does. From 0 to 1.
float tfbm(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * tnoise(q, period);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// Turbulence: octaves folded at their middle, each leaving a sharp crease.
// From 0 to about 1.
float turbulence(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * abs(tnoise(q, period) * 2.0 - 1.0);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// ---- The two solids ----------------------------------------------------------------------------------------------
// Wood: a log along z, its heart off to one side. The distance from the
// heart, bent by noise, counts the rings: pale growth darkening to its end.
float3 wood(float3 q, float rings, float warp) {
float3 earlywood = float3(0.780, 0.500, 0.280);
float3 latewood = float3(0.330, 0.150, 0.070);
float2 heart = float2(-1.30, -0.90) + 0.06 * q.z;
float bend = (tfbm(q * float3(1.6, 1.6, 0.5), 1e4) - 0.5) * warp * 0.9;
float r = length(q.xy - heart) + bend / rings * 4.0;
float ring = fract(r * rings);
float late = smoothstep(0.55, 0.92, ring)
* (1.0 - smoothstep(0.94, 1.0, ring));
// Fine grain: noise stretched along the log.
float grain = tnoise(q * float3(34.0, 34.0, 1.2), 1e4);
float3 col = mix(earlywood, latewood, late * 0.85 + grain * 0.25 * warp);
return col * (0.88 + 0.24 * tnoise(q * 3.0 + 7.0, 1e4));
}
// Marble: stripes from sin(v), pushed sideways by turbulence times `push`,
// with a thin gold vein where the sine crosses zero.
float3 marble(float3 q, float v, float push, float period) {
float3 stone = float3(0.960, 0.930, 0.860);
float3 cloud = float3(0.780, 0.700, 0.620);
float3 vein = float3(0.880, 0.480, 0.150);
float turb = turbulence(q, period);
float s = sin(v + turb * push);
float3 col = mix(cloud, stone, 0.3 + 0.7 * smoothstep(-0.4, 0.95, s));
float line = 1.0 - smoothstep(0.0, 0.08, abs(s));
col = mix(col, vein, line * 0.85);
return col;
}
// The block: wood where its own x is below 0, marble above, with a dark
// seam of glue between. q is in the block's own space, so the texture turns
// with it.
float3 blockColour(float3 q, constant Params& p) {
float3 m = marble(q * 2.6, (q.x * 1.7 + q.y * 1.1 - q.z * 0.6) * 6.0,
1.0 + 5.0 * p.veins, 1e4);
float3 w = wood(q, p.rings, p.warp);
float3 col = q.x < 0.0 ? w : m;
return col * mix(0.35, 1.0, smoothstep(0.004, 0.012, abs(q.x)));
}
// ---- The block, turned and cut -----------------------------------------------------------------------------------
// Into the block's own space: turned by `a` about the vertical, tipped by
// `tilt`. toWorld undoes it.
float3 toBlock(float3 v, float a, float tilt) {
v.xz = rot(v.xz, a);
v.yz = rot(v.yz, tilt);
return v;
}
float3 toWorld(float3 v, float a, float tilt) {
v.yz = rot(v.yz, -tilt);
v.xz = rot(v.xz, -a);
return v;
}
// Where a ray in the block's space meets the block, cut by dot(q, n) = c.
// A box is three pairs of planes, the cut one more: the ray is in once past
// every near side, out at the first far side. xyz: the face; w: how far
// along the ray, -1 for a miss.
float4 hitBlock(float3 ro, float3 rd, float3 n, float c) {
float3 r = rd;
r.x = abs(r.x) < 1e-5 ? 1e-5 : r.x;
r.y = abs(r.y) < 1e-5 ? 1e-5 : r.y;
r.z = abs(r.z) < 1e-5 ? 1e-5 : r.z;
float3 t0 = (-HALF - ro) / r, t1 = (HALF - ro) / r;
float3 lo = min(t0, t1), hi = max(t0, t1);
float near = max(max(lo.x, lo.y), lo.z);
float far = min(min(hi.x, hi.y), hi.z);
float3 face = near == lo.x ? float3(-sign(r.x), 0.0, 0.0)
: near == lo.y ? float3(0.0, -sign(r.y), 0.0)
: float3(0.0, 0.0, -sign(r.z));
float dn = dot(r, n), tp = (c - dot(ro, n)) / (abs(dn) < 1e-5 ? 1e-5 : dn);
if (dn < 0.0 && tp > near) { near = tp; face = n; }
if (dn > 0.0) far = min(far, tp);
if (near > far || near < 0.0) return float4(0.0, 0.0, 0.0, -1.0);
return float4(face, near);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float tt = u.time * p.speed;
// A camera above and in front, looking at the block.
float3 target = float3(0.0, 1.0, 0.0);
float3 ro = target + 3.6 * float3(cos(0.4) * sin(0.5), sin(0.4),
cos(0.4) * cos(0.5));
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
float3 up = cross(rt, fw);
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(s.x * rt + s.y * up + 2.8 * fw);
// The block turns and is tipped; the cut sweeps in and out: at c = 1.15
// it misses the block, lower slices deeper.
float3 centre = float3(0.0, 1.0, 0.0);
float a = tt * 0.45, tilt = 0.45;
float3 n = normalize(float3(0.55, 0.75, 0.6));
float c = 1.15 - p.cut * 1.55 * (0.5 - 0.5 * cos(tt * 0.7));
// The ray in the block's own space, so the solid turns with the block.
float3 lro = toBlock(ro - centre, a, tilt);
float3 lrd = toBlock(rd, a, tilt);
float4 hb = hitBlock(lro, lrd, n, c);
float3 col = float3(0.035, 0.022, 0.024);
if (hb.w > 0.0) {
float3 q = lro + lrd * hb.w;
float3 nrm = toWorld(hb.xyz, a, tilt);
float3 light = normalize(float3(-0.55, 0.85, 0.45));
float diff = max(dot(nrm, light), 0.0);
col = blockColour(q, p) * (diff * 0.9 + 0.2);
}
return float4(col, 1.0);
}5 lines from stage 2 that are gone
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 q = float3(s, u.time * p.speed * 0.4);
⋯
col *= mix(0.35, 1.0, smoothstep(0.004, 0.012, abs(q.x)));Stage 4 · Wood & Marble
The last step lays the floor and lights the block.
- The floor. Slabs 1.25 wide, every one the same slab,
fract(xz / 1.25). Its marble istnoiseon a lattice of 3 cubes to a slab with aperiodof 3, and stripes with whole turns across it,TAU * (f.x + f.y), so the noise and the stripes at one edge of the slab match the other edge and the veins run on over the grout.tilingblends that with the same marble on a lattice that never wraps: at 0 every slab's veins stop at its edges, and the seams show. - The shadow. For a point on the floor,
hitBlockis asked again, from the point towards the light: if the way to the light meets the block, the point is in shadow. Right under the block it is darker still. - The finish. Marble is polished (a tight, bright highlight) and wood satin (a broad, faint one). The fresh cut is a little paler than the faces the air has darkened, and duller. The faces turned down catch the kiln's heat from below, and edges seen side on glow a little.
- The pointer. Its offset from the middle of the picture turns the camera round the block and tips it up or down. In a video the pointer is in the middle; in the playground, move it over the picture, or click it and steer with the arrow keys.
shade(), from the finished shader
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float tt = u.time * p.speed;
// The pointer's offset from the middle turns the camera and tips it.
float2 steer = u.mouse / u.resolution - 0.5;
float yaw = 0.5 + steer.x * TAU * 0.5;
float pitch = clamp(0.40 + steer.y * 1.6, 0.08, 1.3);
float3 target = float3(0.0, 0.75, 0.0);
float3 ro = target + 4.7 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
float3 up = cross(rt, fw);
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(s.x * rt + s.y * up + 2.8 * fw);
// The block floats, turning and tipped, and the cut sweeps in and out:
// at c = 1.15 it misses the block, lower slices deeper.
float3 centre = float3(0.0, 1.0 + 0.05 * sin(tt * 0.9), 0.0);
float a = tt * 0.45, tilt = 0.45;
float3 n = normalize(float3(0.55, 0.75, 0.6));
float c = 1.15 - p.cut * 1.55 * (0.5 - 0.5 * cos(tt * 0.7));
float3 light = normalize(float3(-0.55, 0.85, 0.45));
float3 lightCol = float3(1.00, 0.90, 0.78);
float3 glow = float3(0.90, 0.36, 0.10);
// Behind everything, the kiln's dark with heat low down.
float3 col = mix(float3(0.035, 0.022, 0.024), glow * 0.35,
exp(-max(rd.y + 0.05, 0.0) * 9.0));
float3 lro = toBlock(ro - centre, a, tilt);
float3 lrd = toBlock(rd, a, tilt);
float4 hb = hitBlock(lro, lrd, n, c);
float tf = rd.y < 0.0 ? -ro.y / rd.y : -1.0;
if (hb.w > 0.0 && (tf < 0.0 || hb.w < tf)) {
float3 q = lro + lrd * hb.w;
float3 nrm = toWorld(hb.xyz, a, tilt);
bool isCut = all(hb.xyz == n);
// A fresh cut is a little paler than the faces the air has darkened.
float3 base = blockColour(q, p) * (isCut ? 1.08 : 1.0);
// Wood is satin, marble polished; the fresh cut is rougher.
bool isMarble = q.x >= 0.0;
float shine = isMarble ? 60.0 : 14.0;
float gloss = (isMarble ? 0.55 : 0.18) * (isCut ? 0.4 : 1.0);
float diff = max(dot(nrm, light), 0.0);
float3 h = normalize(light - rd);
float spec = pow(max(dot(nrm, h), 0.0), shine) * gloss;
float fres = pow(1.0 - max(dot(nrm, -rd), 0.0), 4.0);
// The kiln's heat lights the faces turned down and away.
float bounce = max(-nrm.y, 0.0) * 0.5 + 0.25;
col = base * (lightCol * diff * 1.05 + glow * bounce * 0.45 + 0.16)
+ lightCol * spec + glow * fres * 0.25;
} else if (tf > 0.0) {
float3 pos = ro + rd * tf;
float3 base = floorColour(pos.xz, p.tiling, p.veins);
// The block's shadow: does the way to the light meet it?
float4 sh = hitBlock(toBlock(pos - centre, a, tilt),
toBlock(light, a, tilt), n, c);
float lit = sh.w > 0.0 ? 0.3 : 1.0;
// Darker straight under the block, where little light reaches.
float under = 1.0 - 0.45 * exp(-length(pos.xz) * 1.6);
float3 h = normalize(light - rd);
float spec = pow(max(h.y, 0.0), 80.0) * 0.35 * lit;
col = base * (lightCol * light.y * lit * 0.9 + 0.12) * under
+ lightCol * spec;
// The floor fades into the kiln's dark far off.
col = mix(col, float3(0.035, 0.022, 0.024) + glow * 0.12,
smoothstep(4.0, 13.0, tf));
}
col = col / (1.0 + col * 0.25);
return float4(col, 1.0);
}episode-21-wood-marble.metal, the whole file93 new lines since stage 3
// Ray Kiln · Episode 21 · Wood & Marble
//
// @episode 21 Wood & Marble
// @length 60
// @variation 30
// @variations 1,2,3,4,5,6,7,9
// @still 27.0
// @teaches Solid procedural textures, noise that tiles
// @category Fragment
// @tags solid texture, value noise, fbm, wood grain, marble, turbulence, periodic noise, procedural sound
// @final Wood & Marble | A block, half wood and half marble, turning over a floor of marble slabs while a plane slices through it, the grain running on inside wherever the cut goes.
//
// A block carved from two solids: one half wood, one half marble. Neither is a picture wrapped round the block: each is a
// colour for every point in space, worked out from the point itself, so the block is cut out of it like a statue from
// stone. A plane slices through the block and sweeps back and forth, and the face it leaves shows the rings and veins
// carrying on inside. The floor is laid with slabs that are all the same slab, cut from marble whose noise repeats
// across each slab, so the slabs meet with no seam.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 0.5 0.0 2.0 How fast the block turns and the cut sweeps
// @param rings 9.0 3.0 20.0 How many tree rings to a unit of wood
// @param warp 0.5 0.0 1.0 How far noise bends the rings and the grain
// @param veins 0.5 0.0 1.0 How turbulent the marble's veins are
// @param cut 0.6 0.0 1.0 How deep the slicing plane reaches into the block
// @param tiling 1.0 0.0 1.0 From floor slabs that show their seams to marble that tiles with none
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// Half the block's size.
constant float HALF = 0.62;
// Episode 3's rotation.
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// Episode 17's wrap: x into 0 to m, below 0 too.
float wrap(float x, float m) {
return x - m * floor(x / m);
}
// ---- Noise in three dimensions -----------------------------------------------------------------------------------
// Episode 8's hash: a random number from 0 to 1 for each whole-numbered
// cube of space.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
// Value noise in 3D: a random value at every corner of a lattice of cubes,
// blended smoothly across each cube. The lattice wraps every `period` cubes
// (a whole number), so the noise repeats; a huge period never shows it.
float tnoise(float3 q, float period) {
float3 i = floor(q), f = fract(q);
f = f * f * (3.0 - 2.0 * f);
float3 a = float3(wrap(i.x, period), wrap(i.y, period),
wrap(i.z, period));
float3 b = float3(wrap(i.x + 1.0, period), wrap(i.y + 1.0, period),
wrap(i.z + 1.0, period));
float n000 = hash31(float3(a.x, a.y, a.z));
float n100 = hash31(float3(b.x, a.y, a.z));
float n010 = hash31(float3(a.x, b.y, a.z));
float n110 = hash31(float3(b.x, b.y, a.z));
float n001 = hash31(float3(a.x, a.y, b.z));
float n101 = hash31(float3(b.x, a.y, b.z));
float n011 = hash31(float3(a.x, b.y, b.z));
float n111 = hash31(float3(b.x, b.y, b.z));
float lo = mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y);
float hi = mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y);
return mix(lo, hi, f.z);
}
// Four octaves, each lattice twice as fine and its period twice as long,
// so it still repeats where the first one does. From 0 to 1.
float tfbm(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * tnoise(q, period);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// Turbulence: octaves folded at their middle, each leaving a sharp crease.
// From 0 to about 1.
float turbulence(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * abs(tnoise(q, period) * 2.0 - 1.0);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// ---- The two solids ----------------------------------------------------------------------------------------------
// Wood: a log along z, its heart off to one side. The distance from the
// heart, bent by noise, counts the rings: pale growth darkening to its end.
float3 wood(float3 q, float rings, float warp) {
float3 earlywood = float3(0.780, 0.500, 0.280);
float3 latewood = float3(0.330, 0.150, 0.070);
float2 heart = float2(-1.30, -0.90) + 0.06 * q.z;
float bend = (tfbm(q * float3(1.6, 1.6, 0.5), 1e4) - 0.5) * warp * 0.9;
float r = length(q.xy - heart) + bend / rings * 4.0;
float ring = fract(r * rings);
float late = smoothstep(0.55, 0.92, ring)
* (1.0 - smoothstep(0.94, 1.0, ring));
// Fine grain: noise stretched along the log.
float grain = tnoise(q * float3(34.0, 34.0, 1.2), 1e4);
float3 col = mix(earlywood, latewood, late * 0.85 + grain * 0.25 * warp);
return col * (0.88 + 0.24 * tnoise(q * 3.0 + 7.0, 1e4));
}
// Marble: stripes from sin(v), pushed sideways by turbulence times `push`,
// with a thin gold vein where the sine crosses zero.
float3 marble(float3 q, float v, float push, float period) {
float3 stone = float3(0.960, 0.930, 0.860);
float3 cloud = float3(0.780, 0.700, 0.620);
float3 vein = float3(0.880, 0.480, 0.150);
float turb = turbulence(q, period);
float s = sin(v + turb * push);
float3 col = mix(cloud, stone, 0.3 + 0.7 * smoothstep(-0.4, 0.95, s));
float line = 1.0 - smoothstep(0.0, 0.08, abs(s));
col = mix(col, vein, line * 0.85);
return col;
}
// The block: wood where its own x is below 0, marble above, with a dark
// seam of glue between. q is in the block's own space, so the texture turns
// with it.
float3 blockColour(float3 q, constant Params& p) {
float3 m = marble(q * 2.6, (q.x * 1.7 + q.y * 1.1 - q.z * 0.6) * 6.0,
1.0 + 5.0 * p.veins, 1e4);
float3 w = wood(q, p.rings, p.warp);
float3 col = q.x < 0.0 ? w : m;
return col * mix(0.35, 1.0, smoothstep(0.004, 0.012, abs(q.x)));
}
// The floor: slabs 1.25 wide, all the same slab, with grout between. Its
// noise wraps every 3 cubes, 3 to a slab, and its stripes make whole turns
// across it, so the edges match. With tiling at 0 the seams show.
float3 floorColour(float2 xz, float tiling, float veins) {
float2 f = fract(xz / 1.25);
float3 q = float3(f.x, 0.37, f.y) * 3.0;
float v = TAU * (f.x + f.y);
float push = 3.0 + 6.0 * veins;
float3 tiled = marble(q, v, push, 3.0);
float3 cut = marble(q, v, push, 1e4);
float2 e = min(f, 1.0 - f);
float grout = smoothstep(0.0, 0.008, min(e.x, e.y));
return mix(cut, tiled, tiling) * float3(0.60, 0.53, 0.50)
* (0.55 + 0.45 * grout);
}
// ---- The block, turned and cut -----------------------------------------------------------------------------------
// Into the block's own space: turned by `a` about the vertical, tipped by
// `tilt`. toWorld undoes it.
float3 toBlock(float3 v, float a, float tilt) {
v.xz = rot(v.xz, a);
v.yz = rot(v.yz, tilt);
return v;
}
float3 toWorld(float3 v, float a, float tilt) {
v.yz = rot(v.yz, -tilt);
v.xz = rot(v.xz, -a);
return v;
}
// Where a ray in the block's space meets the block, cut by dot(q, n) = c.
// A box is three pairs of planes, the cut one more: the ray is in once past
// every near side, out at the first far side. xyz: the face; w: how far
// along the ray, -1 for a miss.
float4 hitBlock(float3 ro, float3 rd, float3 n, float c) {
float3 r = rd;
r.x = abs(r.x) < 1e-5 ? 1e-5 : r.x;
r.y = abs(r.y) < 1e-5 ? 1e-5 : r.y;
r.z = abs(r.z) < 1e-5 ? 1e-5 : r.z;
float3 t0 = (-HALF - ro) / r, t1 = (HALF - ro) / r;
float3 lo = min(t0, t1), hi = max(t0, t1);
float near = max(max(lo.x, lo.y), lo.z);
float far = min(min(hi.x, hi.y), hi.z);
float3 face = near == lo.x ? float3(-sign(r.x), 0.0, 0.0)
: near == lo.y ? float3(0.0, -sign(r.y), 0.0)
: float3(0.0, 0.0, -sign(r.z));
float dn = dot(r, n), tp = (c - dot(ro, n)) / (abs(dn) < 1e-5 ? 1e-5 : dn);
if (dn < 0.0 && tp > near) { near = tp; face = n; }
if (dn > 0.0) far = min(far, tp);
if (near > far || near < 0.0) return float4(0.0, 0.0, 0.0, -1.0);
return float4(face, near);
}
// ---- The picture -------------------------------------------------------------------------------------------------
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float tt = u.time * p.speed;
// The pointer's offset from the middle turns the camera and tips it.
float2 steer = u.mouse / u.resolution - 0.5;
float yaw = 0.5 + steer.x * TAU * 0.5;
float pitch = clamp(0.40 + steer.y * 1.6, 0.08, 1.3);
float3 target = float3(0.0, 0.75, 0.0);
float3 ro = target + 4.7 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
float3 up = cross(rt, fw);
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(s.x * rt + s.y * up + 2.8 * fw);
// The block floats, turning and tipped, and the cut sweeps in and out:
// at c = 1.15 it misses the block, lower slices deeper.
float3 centre = float3(0.0, 1.0 + 0.05 * sin(tt * 0.9), 0.0);
float a = tt * 0.45, tilt = 0.45;
float3 n = normalize(float3(0.55, 0.75, 0.6));
float c = 1.15 - p.cut * 1.55 * (0.5 - 0.5 * cos(tt * 0.7));
float3 light = normalize(float3(-0.55, 0.85, 0.45));
float3 lightCol = float3(1.00, 0.90, 0.78);
float3 glow = float3(0.90, 0.36, 0.10);
// Behind everything, the kiln's dark with heat low down.
float3 col = mix(float3(0.035, 0.022, 0.024), glow * 0.35,
exp(-max(rd.y + 0.05, 0.0) * 9.0));
float3 lro = toBlock(ro - centre, a, tilt);
float3 lrd = toBlock(rd, a, tilt);
float4 hb = hitBlock(lro, lrd, n, c);
float tf = rd.y < 0.0 ? -ro.y / rd.y : -1.0;
if (hb.w > 0.0 && (tf < 0.0 || hb.w < tf)) {
float3 q = lro + lrd * hb.w;
float3 nrm = toWorld(hb.xyz, a, tilt);
bool isCut = all(hb.xyz == n);
// A fresh cut is a little paler than the faces the air has darkened.
float3 base = blockColour(q, p) * (isCut ? 1.08 : 1.0);
// Wood is satin, marble polished; the fresh cut is rougher.
bool isMarble = q.x >= 0.0;
float shine = isMarble ? 60.0 : 14.0;
float gloss = (isMarble ? 0.55 : 0.18) * (isCut ? 0.4 : 1.0);
float diff = max(dot(nrm, light), 0.0);
float3 h = normalize(light - rd);
float spec = pow(max(dot(nrm, h), 0.0), shine) * gloss;
float fres = pow(1.0 - max(dot(nrm, -rd), 0.0), 4.0);
// The kiln's heat lights the faces turned down and away.
float bounce = max(-nrm.y, 0.0) * 0.5 + 0.25;
col = base * (lightCol * diff * 1.05 + glow * bounce * 0.45 + 0.16)
+ lightCol * spec + glow * fres * 0.25;
} else if (tf > 0.0) {
float3 pos = ro + rd * tf;
float3 base = floorColour(pos.xz, p.tiling, p.veins);
// The block's shadow: does the way to the light meet it?
float4 sh = hitBlock(toBlock(pos - centre, a, tilt),
toBlock(light, a, tilt), n, c);
float lit = sh.w > 0.0 ? 0.3 : 1.0;
// Darker straight under the block, where little light reaches.
float under = 1.0 - 0.45 * exp(-length(pos.xz) * 1.6);
float3 h = normalize(light - rd);
float spec = pow(max(h.y, 0.0), 80.0) * 0.35 * lit;
col = base * (lightCol * light.y * lit * 0.9 + 0.12) * under
+ lightCol * spec;
// The floor fades into the kiln's dark far off.
col = mix(col, float3(0.035, 0.022, 0.024) + glow * 0.12,
smoothstep(4.0, 13.0, tf));
}
col = col / (1.0 + col * 0.25);
return float4(col, 1.0);
}
// ---- Sound -------------------------------------------------------------------------------------------------------
// A random number from a whole number.
float hash11(float n) {
return fract(sin(n * 12.9898) * 43758.5453);
}
// One note of a pattern of 16 steps. Wood knocks low and dies fast, its
// fourth harmonic tuned as a marimba bar's is; marble rings high and long,
// with partials out of tune with each other, as stone does.
float2 note(float age, float key) {
if (hash11(key * 3.1 + 0.7) < 0.35) return float2(0.0);
bool stone = hash11(key * 5.3 + 1.9) < 0.4;
float deg = floor(hash11(key * 7.7 + 4.2) * 5.0);
float semis = deg < 1.0 ? 0.0 : deg < 2.0 ? 3.0 : deg < 3.0 ? 5.0
: deg < 4.0 ? 7.0 : 10.0;
float f = (stone ? 880.0 : 146.8) * exp2(semis / 12.0);
float v;
if (stone) {
v = 0.45 * sin(TAU * f * age) * exp(-age * 5.0)
+ 0.25 * sin(TAU * f * 2.32 * age) * exp(-age * 9.0)
+ 0.15 * sin(TAU * f * 4.17 * age) * exp(-age * 15.0);
} else {
v = sin(TAU * f * age) * exp(-age * 7.0)
+ 0.35 * sin(TAU * f * 4.0 * age) * exp(-age * 22.0);
}
v *= smoothstep(0.0, 0.003, age);
float pan = 0.5 + 0.35 * (hash11(key * 9.1) - 0.5) * 2.0;
return v * float2(1.0 - pan, pan);
}
float2 sound(float t, constant Params& p) {
// Steps faster with speed. With tiling up, the pattern's 16 steps repeat
// like the floor's slabs; with it down, every step is new.
float rate = 2.5 + 2.0 * p.speed;
float x = t * rate;
float2 knocks = float2(0.0);
// A note rings on for four steps, by when even stone has died away.
for (int k = 0; k < 4; k++) {
float i = floor(x) - float(k);
if (i < 0.0) continue;
float age = (x - i) / rate;
float key = p.tiling > 0.5 ? wrap(i, 16.0) : i;
knocks += note(age, key);
}
// Under it, a hum of the marble's own two notes, swelling as the cut
// goes in and out.
float swell = 0.5 + 0.5 * cos(t * p.speed * 0.7);
float hum = sin(TAU * 110.0 * t) + 0.6 * sin(TAU * 164.8 * t + 0.3)
+ 0.25 * sin(TAU * 220.0 * t * 1.003);
hum *= 0.14 * (0.4 + 0.6 * swell * p.cut);
float2 mixdown = knocks * 0.8 + float2(hum);
float fadeIn = smoothstep(0.0, 1.5, t);
return tanh(mixdown * 1.4) * 0.7 * fadeIn * p.volume;
}8 lines from stage 3 that are gone
float3 target = float3(0.0, 1.0, 0.0);
float3 ro = target + 3.6 * float3(cos(0.4) * sin(0.5), sin(0.4),
cos(0.4) * cos(0.5));
⋯
float3 centre = float3(0.0, 1.0, 0.0);
⋯
float3 col = float3(0.035, 0.022, 0.024);
if (hb.w > 0.0) {
⋯
float3 light = normalize(float3(-0.55, 0.85, 0.45));
⋯
col = blockColour(q, p) * (diff * 0.9 + 0.2);The sound
The sound repeats the way the floor does. It steps through a pattern 2.5 + 2 × speed times a second, and each step's note comes from a hash of the step's number. With tiling over a half, the number is wrapped by 16 first, so the pattern of 16 steps comes round again and again, exactly; with it under a half every step is new, and the pattern never repeats.
About a third of the steps rest. Of the rest, some are wood: a knock on a D, low, that dies in a fraction of a second, with its fourth harmonic beside it as a marimba bar is tuned. The others are marble: a high ring on an A, two octaves and more up, with partials at 2.32 and 4.17 times the note, out of tune with each other as stone is, that dies more slowly. Both take their notes from the same five of a minor pentatonic scale, and each is placed a little left or right. Under them hum three low notes, swelling as the cut goes in and out, more for a deeper cut. It all goes through tanh and fades in over the first second and a half. At the defaults the peak is about -6.7 dBFS and the average (RMS) about -20.8 dBFS.
New in Ray Kiln
Colour swatches in the code. In the playground's editor, every float3(r, g, b) written as three plain numbers now has a small square of its colour in front of it. Click the square and the browser's colour picker opens on that colour; as you pick, the new colour is written back into the three numbers, the shader recompiles and the picture follows. It is an ordinary edit, so ⌘Z takes it back. The swatch shows the numbers as the screen shows them before any light: each number times 255, as the canvas stores it. A colour brighter than white shows its hue, and a picked colour is scaled back up to the same brightness, so a glow keeps its strength. Any float3 of three numbers none of them negative gets a swatch, so a position such as float3(0.0, 1.0, 0.0) gets one too.
This episode's colours are written that way. Open Wood & Marble in the playground and find earlywood and latewood in wood, and stone, cloud and vein in marble: click vein's swatch and pick a blue, and the gold veins in the block and the floor turn blue together, because both come from the same function. Give latewood the colour of earlywood and the rings vanish.
Try this
- Set
cutto 1: the plane slices deep into the block, past its middle, and the face it leaves shows rings and veins the outside never did. - Set
tilingto 0 and look at the floor: the veins stop dead at every edge of a slab. Back at 1, they run on over the grout. - Set
ringsto 3, then to 20: a few broad rings, then fine close grain like a slow-grown hardwood. - Set
warpto 0: the rings are perfect circles round the heart, and the streaks of grain go. - Set
veinsto 0, then 1: the marble's stripes run nearly straight, then crease and tangle. - In
wood, changefloat2 heart = float2(-1.30, -0.90)tofloat2 heart = float2(0.0, 0.0): the heart of the log is in the middle of the block, and the end grain shows rings all the way round it. - In
blockColour, changefloat3 col = q.x < 0.0 ? w : m;tofloat3 col = q.y < 0.0 ? w : m;: the block is split across instead, wood below and marble above. - In
floorColour, changefloat v = TAU * (f.x + f.y);tofloat v = TAU * (f.x + 1.5 * f.y);: one and a half turns don't fit a slab from front to back, and the stripes break at the seams between its rows even withtilingat 1.
Parameters
Pick a parameter and drag the slider. Each frame is the same moment of the shader, with only that one setting changed.
| Parameter | Default | Range | What it does |
|---|---|---|---|
speed | 0.5 | 0.0 to 2.0 | How fast the block turns and the cut sweeps |
rings | 9.0 | 3.0 to 20.0 | How many tree rings to a unit of wood |
warp | 0.5 | 0.0 to 1.0 | How far noise bends the rings and the grain |
veins | 0.5 | 0.0 to 1.0 | How turbulent the marble's veins are |
cut | 0.6 | 0.0 to 1.0 | How deep the slicing plane reaches into the block |
tiling | 1.0 | 0.0 to 1.0 | From floor slabs that show their seams to marble that tiles with none |
volume | 0.8 | 0.0 to 1.0 | Loudness of the sound |
More from this shader
The same shader 8 more ways: each from a different moment, with different settings, one after another. Each chapter below says which settings moved most, then lists them all.
Wallpapers
A still from the shader, rendered at full size for a desktop or a phone screen. Free to use as your wallpaper.
Clips to post
Fifteen seconds of the shader with its sound, sized for posting: square for feeds, vertical for stories and short videos. Free to post, ideally with a link to this page.
Made from this episode
The full source
This is the whole episode: the picture, the parameters and the sound.
episode-21-wood-marble.metal
// Ray Kiln · Episode 21 · Wood & Marble
//
// @episode 21 Wood & Marble
// @length 60
// @variation 30
// @variations 1,2,3,4,5,6,7,9
// @still 27.0
// @teaches Solid procedural textures, noise that tiles
// @category Fragment
// @tags solid texture, value noise, fbm, wood grain, marble, turbulence, periodic noise, procedural sound
// @final Wood & Marble | A block, half wood and half marble, turning over a floor of marble slabs while a plane slices through it, the grain running on inside wherever the cut goes.
//
// A block carved from two solids: one half wood, one half marble. Neither is a picture wrapped round the block: each is a
// colour for every point in space, worked out from the point itself, so the block is cut out of it like a statue from
// stone. A plane slices through the block and sweeps back and forth, and the face it leaves shows the rings and veins
// carrying on inside. The floor is laid with slabs that are all the same slab, cut from marble whose noise repeats
// across each slab, so the slabs meet with no seam.
// Each @param line below becomes a slider in the host app and a row in the page's parameters table:
// // @param name default min max description
// @param speed 0.5 0.0 2.0 How fast the block turns and the cut sweeps
// @param rings 9.0 3.0 20.0 How many tree rings to a unit of wood
// @param warp 0.5 0.0 1.0 How far noise bends the rings and the grain
// @param veins 0.5 0.0 1.0 How turbulent the marble's veins are
// @param cut 0.6 0.0 1.0 How deep the slicing plane reaches into the block
// @param tiling 1.0 0.0 1.0 From floor slabs that show their seams to marble that tiles with none
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// Half the block's size.
constant float HALF = 0.62;
// Episode 3's rotation.
float2 rot(float2 p, float a) {
float c = cos(a), s = sin(a);
return float2(c * p.x - s * p.y, s * p.x + c * p.y);
}
// Episode 17's wrap: x into 0 to m, below 0 too.
float wrap(float x, float m) {
return x - m * floor(x / m);
}
// ---- Noise in three dimensions -----------------------------------------------------------------------------------
// Episode 8's hash: a random number from 0 to 1 for each whole-numbered
// cube of space.
float hash31(float3 p) {
uint3 v = uint3(int3(floor(p))) * 1664525u + 1013904223u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
v ^= v >> 16u;
v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
return float(v.x) * (1.0 / 4294967295.0);
}
// Value noise in 3D: a random value at every corner of a lattice of cubes,
// blended smoothly across each cube. The lattice wraps every `period` cubes
// (a whole number), so the noise repeats; a huge period never shows it.
float tnoise(float3 q, float period) {
float3 i = floor(q), f = fract(q);
f = f * f * (3.0 - 2.0 * f);
float3 a = float3(wrap(i.x, period), wrap(i.y, period),
wrap(i.z, period));
float3 b = float3(wrap(i.x + 1.0, period), wrap(i.y + 1.0, period),
wrap(i.z + 1.0, period));
float n000 = hash31(float3(a.x, a.y, a.z));
float n100 = hash31(float3(b.x, a.y, a.z));
float n010 = hash31(float3(a.x, b.y, a.z));
float n110 = hash31(float3(b.x, b.y, a.z));
float n001 = hash31(float3(a.x, a.y, b.z));
float n101 = hash31(float3(b.x, a.y, b.z));
float n011 = hash31(float3(a.x, b.y, b.z));
float n111 = hash31(float3(b.x, b.y, b.z));
float lo = mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y);
float hi = mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y);
return mix(lo, hi, f.z);
}
// Four octaves, each lattice twice as fine and its period twice as long,
// so it still repeats where the first one does. From 0 to 1.
float tfbm(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * tnoise(q, period);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// Turbulence: octaves folded at their middle, each leaving a sharp crease.
// From 0 to about 1.
float turbulence(float3 q, float period) {
float v = 0.0, a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * abs(tnoise(q, period) * 2.0 - 1.0);
q *= 2.0;
period *= 2.0;
a *= 0.5;
}
return v / 0.9375;
}
// ---- The two solids ----------------------------------------------------------------------------------------------
// Wood: a log along z, its heart off to one side. The distance from the
// heart, bent by noise, counts the rings: pale growth darkening to its end.
float3 wood(float3 q, float rings, float warp) {
float3 earlywood = float3(0.780, 0.500, 0.280);
float3 latewood = float3(0.330, 0.150, 0.070);
float2 heart = float2(-1.30, -0.90) + 0.06 * q.z;
float bend = (tfbm(q * float3(1.6, 1.6, 0.5), 1e4) - 0.5) * warp * 0.9;
float r = length(q.xy - heart) + bend / rings * 4.0;
float ring = fract(r * rings);
float late = smoothstep(0.55, 0.92, ring)
* (1.0 - smoothstep(0.94, 1.0, ring));
// Fine grain: noise stretched along the log.
float grain = tnoise(q * float3(34.0, 34.0, 1.2), 1e4);
float3 col = mix(earlywood, latewood, late * 0.85 + grain * 0.25 * warp);
return col * (0.88 + 0.24 * tnoise(q * 3.0 + 7.0, 1e4));
}
// Marble: stripes from sin(v), pushed sideways by turbulence times `push`,
// with a thin gold vein where the sine crosses zero.
float3 marble(float3 q, float v, float push, float period) {
float3 stone = float3(0.960, 0.930, 0.860);
float3 cloud = float3(0.780, 0.700, 0.620);
float3 vein = float3(0.880, 0.480, 0.150);
float turb = turbulence(q, period);
float s = sin(v + turb * push);
float3 col = mix(cloud, stone, 0.3 + 0.7 * smoothstep(-0.4, 0.95, s));
float line = 1.0 - smoothstep(0.0, 0.08, abs(s));
col = mix(col, vein, line * 0.85);
return col;
}
// The block: wood where its own x is below 0, marble above, with a dark
// seam of glue between. q is in the block's own space, so the texture turns
// with it.
float3 blockColour(float3 q, constant Params& p) {
float3 m = marble(q * 2.6, (q.x * 1.7 + q.y * 1.1 - q.z * 0.6) * 6.0,
1.0 + 5.0 * p.veins, 1e4);
float3 w = wood(q, p.rings, p.warp);
float3 col = q.x < 0.0 ? w : m;
return col * mix(0.35, 1.0, smoothstep(0.004, 0.012, abs(q.x)));
}
// The floor: slabs 1.25 wide, all the same slab, with grout between. Its
// noise wraps every 3 cubes, 3 to a slab, and its stripes make whole turns
// across it, so the edges match. With tiling at 0 the seams show.
float3 floorColour(float2 xz, float tiling, float veins) {
float2 f = fract(xz / 1.25);
float3 q = float3(f.x, 0.37, f.y) * 3.0;
float v = TAU * (f.x + f.y);
float push = 3.0 + 6.0 * veins;
float3 tiled = marble(q, v, push, 3.0);
float3 cut = marble(q, v, push, 1e4);
float2 e = min(f, 1.0 - f);
float grout = smoothstep(0.0, 0.008, min(e.x, e.y));
return mix(cut, tiled, tiling) * float3(0.60, 0.53, 0.50)
* (0.55 + 0.45 * grout);
}
// ---- The block, turned and cut -----------------------------------------------------------------------------------
// Into the block's own space: turned by `a` about the vertical, tipped by
// `tilt`. toWorld undoes it.
float3 toBlock(float3 v, float a, float tilt) {
v.xz = rot(v.xz, a);
v.yz = rot(v.yz, tilt);
return v;
}
float3 toWorld(float3 v, float a, float tilt) {
v.yz = rot(v.yz, -tilt);
v.xz = rot(v.xz, -a);
return v;
}
// Where a ray in the block's space meets the block, cut by dot(q, n) = c.
// A box is three pairs of planes, the cut one more: the ray is in once past
// every near side, out at the first far side. xyz: the face; w: how far
// along the ray, -1 for a miss.
float4 hitBlock(float3 ro, float3 rd, float3 n, float c) {
float3 r = rd;
r.x = abs(r.x) < 1e-5 ? 1e-5 : r.x;
r.y = abs(r.y) < 1e-5 ? 1e-5 : r.y;
r.z = abs(r.z) < 1e-5 ? 1e-5 : r.z;
float3 t0 = (-HALF - ro) / r, t1 = (HALF - ro) / r;
float3 lo = min(t0, t1), hi = max(t0, t1);
float near = max(max(lo.x, lo.y), lo.z);
float far = min(min(hi.x, hi.y), hi.z);
float3 face = near == lo.x ? float3(-sign(r.x), 0.0, 0.0)
: near == lo.y ? float3(0.0, -sign(r.y), 0.0)
: float3(0.0, 0.0, -sign(r.z));
float dn = dot(r, n), tp = (c - dot(ro, n)) / (abs(dn) < 1e-5 ? 1e-5 : dn);
if (dn < 0.0 && tp > near) { near = tp; face = n; }
if (dn > 0.0) far = min(far, tp);
if (near > far || near < 0.0) return float4(0.0, 0.0, 0.0, -1.0);
return float4(face, near);
}
// ---- The picture -------------------------------------------------------------------------------------------------
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float tt = u.time * p.speed;
// The pointer's offset from the middle turns the camera and tips it.
float2 steer = u.mouse / u.resolution - 0.5;
float yaw = 0.5 + steer.x * TAU * 0.5;
float pitch = clamp(0.40 + steer.y * 1.6, 0.08, 1.3);
float3 target = float3(0.0, 0.75, 0.0);
float3 ro = target + 4.7 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(target - ro);
float3 rt = normalize(cross(fw, float3(0.0, 1.0, 0.0)));
float3 up = cross(rt, fw);
float side = min(u.resolution.x, u.resolution.y);
float2 s = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(s.x * rt + s.y * up + 2.8 * fw);
// The block floats, turning and tipped, and the cut sweeps in and out:
// at c = 1.15 it misses the block, lower slices deeper.
float3 centre = float3(0.0, 1.0 + 0.05 * sin(tt * 0.9), 0.0);
float a = tt * 0.45, tilt = 0.45;
float3 n = normalize(float3(0.55, 0.75, 0.6));
float c = 1.15 - p.cut * 1.55 * (0.5 - 0.5 * cos(tt * 0.7));
float3 light = normalize(float3(-0.55, 0.85, 0.45));
float3 lightCol = float3(1.00, 0.90, 0.78);
float3 glow = float3(0.90, 0.36, 0.10);
// Behind everything, the kiln's dark with heat low down.
float3 col = mix(float3(0.035, 0.022, 0.024), glow * 0.35,
exp(-max(rd.y + 0.05, 0.0) * 9.0));
float3 lro = toBlock(ro - centre, a, tilt);
float3 lrd = toBlock(rd, a, tilt);
float4 hb = hitBlock(lro, lrd, n, c);
float tf = rd.y < 0.0 ? -ro.y / rd.y : -1.0;
if (hb.w > 0.0 && (tf < 0.0 || hb.w < tf)) {
float3 q = lro + lrd * hb.w;
float3 nrm = toWorld(hb.xyz, a, tilt);
bool isCut = all(hb.xyz == n);
// A fresh cut is a little paler than the faces the air has darkened.
float3 base = blockColour(q, p) * (isCut ? 1.08 : 1.0);
// Wood is satin, marble polished; the fresh cut is rougher.
bool isMarble = q.x >= 0.0;
float shine = isMarble ? 60.0 : 14.0;
float gloss = (isMarble ? 0.55 : 0.18) * (isCut ? 0.4 : 1.0);
float diff = max(dot(nrm, light), 0.0);
float3 h = normalize(light - rd);
float spec = pow(max(dot(nrm, h), 0.0), shine) * gloss;
float fres = pow(1.0 - max(dot(nrm, -rd), 0.0), 4.0);
// The kiln's heat lights the faces turned down and away.
float bounce = max(-nrm.y, 0.0) * 0.5 + 0.25;
col = base * (lightCol * diff * 1.05 + glow * bounce * 0.45 + 0.16)
+ lightCol * spec + glow * fres * 0.25;
} else if (tf > 0.0) {
float3 pos = ro + rd * tf;
float3 base = floorColour(pos.xz, p.tiling, p.veins);
// The block's shadow: does the way to the light meet it?
float4 sh = hitBlock(toBlock(pos - centre, a, tilt),
toBlock(light, a, tilt), n, c);
float lit = sh.w > 0.0 ? 0.3 : 1.0;
// Darker straight under the block, where little light reaches.
float under = 1.0 - 0.45 * exp(-length(pos.xz) * 1.6);
float3 h = normalize(light - rd);
float spec = pow(max(h.y, 0.0), 80.0) * 0.35 * lit;
col = base * (lightCol * light.y * lit * 0.9 + 0.12) * under
+ lightCol * spec;
// The floor fades into the kiln's dark far off.
col = mix(col, float3(0.035, 0.022, 0.024) + glow * 0.12,
smoothstep(4.0, 13.0, tf));
}
col = col / (1.0 + col * 0.25);
return float4(col, 1.0);
}
// ---- Sound -------------------------------------------------------------------------------------------------------
// A random number from a whole number.
float hash11(float n) {
return fract(sin(n * 12.9898) * 43758.5453);
}
// One note of a pattern of 16 steps. Wood knocks low and dies fast, its
// fourth harmonic tuned as a marimba bar's is; marble rings high and long,
// with partials out of tune with each other, as stone does.
float2 note(float age, float key) {
if (hash11(key * 3.1 + 0.7) < 0.35) return float2(0.0);
bool stone = hash11(key * 5.3 + 1.9) < 0.4;
float deg = floor(hash11(key * 7.7 + 4.2) * 5.0);
float semis = deg < 1.0 ? 0.0 : deg < 2.0 ? 3.0 : deg < 3.0 ? 5.0
: deg < 4.0 ? 7.0 : 10.0;
float f = (stone ? 880.0 : 146.8) * exp2(semis / 12.0);
float v;
if (stone) {
v = 0.45 * sin(TAU * f * age) * exp(-age * 5.0)
+ 0.25 * sin(TAU * f * 2.32 * age) * exp(-age * 9.0)
+ 0.15 * sin(TAU * f * 4.17 * age) * exp(-age * 15.0);
} else {
v = sin(TAU * f * age) * exp(-age * 7.0)
+ 0.35 * sin(TAU * f * 4.0 * age) * exp(-age * 22.0);
}
v *= smoothstep(0.0, 0.003, age);
float pan = 0.5 + 0.35 * (hash11(key * 9.1) - 0.5) * 2.0;
return v * float2(1.0 - pan, pan);
}
float2 sound(float t, constant Params& p) {
// Steps faster with speed. With tiling up, the pattern's 16 steps repeat
// like the floor's slabs; with it down, every step is new.
float rate = 2.5 + 2.0 * p.speed;
float x = t * rate;
float2 knocks = float2(0.0);
// A note rings on for four steps, by when even stone has died away.
for (int k = 0; k < 4; k++) {
float i = floor(x) - float(k);
if (i < 0.0) continue;
float age = (x - i) / rate;
float key = p.tiling > 0.5 ? wrap(i, 16.0) : i;
knocks += note(age, key);
}
// Under it, a hum of the marble's own two notes, swelling as the cut
// goes in and out.
float swell = 0.5 + 0.5 * cos(t * p.speed * 0.7);
float hum = sin(TAU * 110.0 * t) + 0.6 * sin(TAU * 164.8 * t + 0.3)
+ 0.25 * sin(TAU * 220.0 * t * 1.003);
hum *= 0.14 * (0.4 + 0.6 * swell * p.cut);
float2 mixdown = knocks * 0.8 + float2(hum);
float fadeIn = smoothstep(0.0, 1.5, t);
return tanh(mixdown * 1.4) * 0.7 * fadeIn * p.volume;
}Running it in your own project
Every Ray Kiln shader is the same shape. The standalone download above is the shader with the small wrapper around it that the Ray Kiln host adds, so it compiles with the ordinary Metal compiler and runs in your own app. It defines three entry points: rk_vertex (a full-screen triangle), rk_fragment (calls the shader's shade) and, for shaders with sound, the compute kernel rk_sound.
To draw it, pass the uniforms at fragment buffer 0 and the parameter values at buffer 1, in the order they are declared:
Drawing it, in Swift
struct Uniforms {
var resolution: SIMD2<Float>
var time: Float
var timeDelta: Float
var mouse: SIMD2<Float>
var frame: UInt32
var pad: UInt32 = 0
}
let library = try device.makeLibrary(source: standaloneSource, options: nil)
let descriptor = MTLRenderPipelineDescriptor()
descriptor.vertexFunction = library.makeFunction(name: "rk_vertex")
descriptor.fragmentFunction = library.makeFunction(name: "rk_fragment")
descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
let pipeline = try device.makeRenderPipelineState(descriptor: descriptor)
// Each frame, inside a render pass:
var uniforms = Uniforms(resolution: size, time: time, timeDelta: dt, mouse: mouse, frame: frame)
var params: [Float] = [0.5, 9.0, 0.5, 0.5, 0.6, 1.0, 0.8] // the @param defaults, in the order they are declared
encoder.setRenderPipelineState(pipeline)
encoder.setFragmentBytes(&uniforms, length: MemoryLayout<Uniforms>.stride, index: 0)
encoder.setFragmentBytes(¶ms, length: params.count * MemoryLayout<Float>.stride, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)Time is in seconds, mouse is in pixels from the bottom left, and uv in the shader runs 0 to 1 with (0, 0) at the bottom left. Write the output as sRGB: the shader's numbers go to the screen as they are.
More in Noise & Texture
Press ? for keyboard shortcuts.

