Mandelbulb
Episode 7's fractal in three dimensions. Every point in space is raised to a power again and again, written as a distance and two angles, and the points that never run away make the bulb, buds on buds on buds. A ray can't find a surface like that by stepping blindly, so the same loop also works out how far the ray may safely step. The bulb glows in the kiln's dark, coloured by where each point's orbit went, its power slowly breathing.
Teaches 3D fractals, distance estimators, orbit-trap colour
Builds on 03 · Floating 3D Scene, 07 · Fractal Glow
Watch
The idea
Episode 7 took a complex number, squared it, added the starting point back, and did it again and again; the points that never escaped made the fractal. Squaring a complex number doubles its angle and squares its length. The Mandelbulb does the same in three dimensions: a point is written as a distance r and two angles, theta up from the pole and phi around it, then r is raised to a power, both angles are multiplied by it, and the starting point is added back. With a power of 8 that gives the classic bulb, with buds that carry smaller buds all the way down.
Drawing it takes a second idea. Episode 3 marched a ray towards a shape by its exact distance, but nobody knows the exact distance to a fractal. What can be known is how fast the loop runs away near a point (its derivative, dr), and from that, a distance estimate: a step the ray can always take without going through the surface. Then colour comes from the loop as well: each point's orbit, the path it took while being raised to the power, passed near some places and far from others, and an orbit trap keeps how near it ever came.
It is built in four steps, and each step is a shader you can run on its own.
Stage 1 · The bulb
bulb(p, power) is the whole fractal. Each turn, r = length(z), theta = acos(z.y / r) and phi = atan2(z.z, z.x); then z becomes pow(r, power) times the direction with angles theta * power and phi * power, plus p. A point whose r passes 2 has escaped and won't come back, so the loop stops.
Next to z, the loop keeps dr, how much a small step at p has been stretched so far: each turn it becomes pow(r, power - 1) * power * dr + 1, the derivative of the power plus the + p. When the loop ends, 0.5 * log(r) * r / dr is the distance estimate, an amount the ray can always step without crossing into the bulb. The march takes 90% of it each time, to stay on the safe side, and stops when the bulb is closer than one pixel (pixel * dist, a pixel's width at that distance), so its detail is as fine as the picture can show and no finer. r is never let fall to zero (max(length(z), 1e-6)), since acos(z.y / r) and pow(r, …) have no answer there.
The surface's direction (its normal) is found from four estimates at the corners of a small tetrahedron, a step of half a pixel around the hit, and the bulb is lit like clay, as bright as it faces a light above and to the left of the camera. The camera circles the bulb at a distance of 2.6, rising and falling a little.
stage-1-bulb.metal
// Stage 1 · The bulb
// Episode 7's set in three dimensions. Each turn a point becomes a distance and two angles: the distance goes to the
// power, the angles are multiplied by it, and the start is added back. The points that never escape are the bulb, and
// a ray marches to it by a distance estimate.
// @param power 8.0 3.0 12.0 The power each point is raised to: 3 is a folded lump, 8 the classic bulb, 12 a bristling one
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// z as a distance r and two angles; r to the power, the angles times it,
// and p added back. dr follows how fast z runs away, and from it comes a
// distance the ray can step without crossing the surface.
float bulb(float3 p, float power) {
float3 z = p;
float dr = 1.0;
float r = max(length(z), 1e-6);
for (int i = 0; i < 8; i++) {
if (r > 2.0) break;
float theta = acos(clamp(z.y / r, -1.0, 1.0)) * power;
float phi = atan2(z.z, z.x) * power;
dr = pow(r, power - 1.0) * power * dr + 1.0;
float zr = pow(r, power);
z = zr * float3(sin(theta) * cos(phi), cos(theta),
sin(theta) * sin(phi)) + p;
r = max(length(z), 1e-6);
}
return 0.5 * log(r) * r / dr;
}
// The surface's direction from four samples at a tetrahedron's corners.
float3 bulbNormal(float3 p, float power, float e) {
const float2 k = float2(1.0, -1.0);
return normalize(k.xyy * bulb(p + k.xyy * e, power)
+ k.yyx * bulb(p + k.yyx * e, power)
+ k.yxy * bulb(p + k.yxy * e, power)
+ k.xxx * bulb(p + k.xxx * e, power));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * 0.5;
// The camera circles the bulb, rising and falling a little.
float yaw = t * 0.3;
float pitch = 0.35 + 0.25 * sin(t * 0.17);
float3 ro = 2.6 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(-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 q = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(q.x * rt + q.y * up + 2.0 * fw);
// One pixel's width a unit away: closer than a pixel is close enough.
float pixel = 1.0 / side;
float shift = (p.heat - 0.5) * 0.4;
float back = 0.06 + 0.10 * exp(-dot(q, q) * 0.6);
// March: step by the estimate until the ray is close enough.
float dist = 0.0;
bool hit = false;
for (int i = 0; i < 160; i++) {
float d = bulb(ro + rd * dist, p.power);
if (d < pixel * dist) { hit = true; break; }
dist += d * 0.9;
if (dist > 6.0) break;
}
if (!hit) return float4(kiln(back + shift), 1.0);
// Lit like clay: as bright as the surface faces the light.
float3 n = bulbNormal(ro + rd * dist, p.power, pixel * dist * 0.5);
float3 l = normalize(up * 0.8 - rt * 0.5 - fw * 0.4);
float lit = max(dot(n, l), 0.0);
return float4(kiln(0.12 + 0.6 * lit + shift), 1.0);
}Stage 2 · Shade in the folds
With only that light, the deep folds between the buds are as bright as the tips of them. occlusion is cheap ambient occlusion: from the hit it steps out along the normal five times, from 0.04 to 0.16 away, and asks the distance estimate how far the bulb is from each point. Out in the open the bulb is as far as the step; deep in a fold it is nearer, and each shortfall darkens the point a little, the nearest steps counting most. A rim light, pow(1 - dot(n, -rd), 3), adds a little glow on the edges that turn away from the camera.
stage-2-folds.metal16 new lines since stage 1
// Stage 2 · Shade in the folds
// Light alone leaves the bulb flat. Ambient occlusion steps out from the surface along its normal and asks the
// distance estimate how near the bulb is: deep in a fold it is nearer than the step, and the fold darkens.
// @param power 8.0 3.0 12.0 The power each point is raised to: 3 is a folded lump, 8 the classic bulb, 12 a bristling one
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// z as a distance r and two angles; r to the power, the angles times it,
// and p added back. dr follows how fast z runs away, and from it comes a
// distance the ray can step without crossing the surface.
float bulb(float3 p, float power) {
float3 z = p;
float dr = 1.0;
float r = max(length(z), 1e-6);
for (int i = 0; i < 8; i++) {
if (r > 2.0) break;
float theta = acos(clamp(z.y / r, -1.0, 1.0)) * power;
float phi = atan2(z.z, z.x) * power;
dr = pow(r, power - 1.0) * power * dr + 1.0;
float zr = pow(r, power);
z = zr * float3(sin(theta) * cos(phi), cos(theta),
sin(theta) * sin(phi)) + p;
r = max(length(z), 1e-6);
}
return 0.5 * log(r) * r / dr;
}
// The surface's direction from four samples at a tetrahedron's corners.
float3 bulbNormal(float3 p, float power, float e) {
const float2 k = float2(1.0, -1.0);
return normalize(k.xyy * bulb(p + k.xyy * e, power)
+ k.yyx * bulb(p + k.yyx * e, power)
+ k.yxy * bulb(p + k.yxy * e, power)
+ k.xxx * bulb(p + k.xxx * e, power));
}
// Ambient occlusion: step out along the normal; where the bulb is nearer
// than the step, the room is shut in, and it darkens.
float occlusion(float3 p, float3 n, float power) {
float shut = 0.0;
float weight = 1.0;
for (int i = 1; i <= 5; i++) {
float h = 0.01 + 0.03 * float(i);
shut += (h - bulb(p + n * h, power)) * weight;
weight *= 0.75;
}
return clamp(1.0 - 3.0 * shut, 0.0, 1.0);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * 0.5;
// The camera circles the bulb, rising and falling a little.
float yaw = t * 0.3;
float pitch = 0.35 + 0.25 * sin(t * 0.17);
float3 ro = 2.6 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(-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 q = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(q.x * rt + q.y * up + 2.0 * fw);
// One pixel's width a unit away: closer than a pixel is close enough.
float pixel = 1.0 / side;
float shift = (p.heat - 0.5) * 0.4;
float back = 0.06 + 0.10 * exp(-dot(q, q) * 0.6);
// March: step by the estimate until the ray is close enough.
float dist = 0.0;
bool hit = false;
for (int i = 0; i < 160; i++) {
float d = bulb(ro + rd * dist, p.power);
if (d < pixel * dist) { hit = true; break; }
dist += d * 0.9;
if (dist > 6.0) break;
}
if (!hit) return float4(kiln(back + shift), 1.0);
float3 pos = ro + rd * dist;
float3 n = bulbNormal(pos, p.power, pixel * dist * 0.5);
float3 l = normalize(up * 0.8 - rt * 0.5 - fw * 0.4);
float lit = max(dot(n, l), 0.0);
float ao = occlusion(pos, n, p.power);
// The rim: edges turning away from the camera catch the kiln's glow.
float rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
float level = (0.35 + 0.65 * lit) * (0.4 + 0.6 * ao) + 0.12 * rim;
return float4(kiln(0.15 + 0.75 * level + shift), 1.0);
}2 lines from stage 1 that are gone
float3 n = bulbNormal(ro + rd * dist, p.power, pixel * dist * 0.5);
⋯
return float4(kiln(0.12 + 0.6 * lit + shift), 1.0);Stage 3 · Orbit traps
Now the loop also keeps trap, the nearest any z of the orbit came to the plane y = 0, the bulb's equator. Points whose orbit passed close to it run hot (orbit = 1 - sqrt(trap * 1.2), squared, brightens them), and points whose orbit kept away from it stay dark, so the colour follows the fractal's own structure: veins and rings that repeat on every bud. trap mixes it with the plain light and shade from stage 2. The trap is a smooth distance rather than a count of turns, so it has no hard bands.
stage-3-trap.metal18 new lines since stage 2
// Stage 3 · Orbit traps
// Colour from the orbit itself. As a point is raised to the power again and again, its path passes near some places
// and far from others; the trap keeps how near it ever came to the plane y = 0. Near runs hot, far stays dark.
// @param power 8.0 3.0 12.0 The power each point is raised to: 3 is a folded lump, 8 the classic bulb, 12 a bristling one
// @param trap 0.7 0.0 1.0 How much the orbit trap colours the bulb, against plain light and shade
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// The bulb as before, and its orbit trap: the nearest the orbit came to the
// plane y = 0, the bulb's equator.
float bulb(float3 p, float power, thread float& trap) {
float3 z = p;
float dr = 1.0;
float r = max(length(z), 1e-6);
trap = 1e3;
for (int i = 0; i < 8; i++) {
if (r > 2.0) break;
float theta = acos(clamp(z.y / r, -1.0, 1.0)) * power;
float phi = atan2(z.z, z.x) * power;
dr = pow(r, power - 1.0) * power * dr + 1.0;
float zr = pow(r, power);
z = zr * float3(sin(theta) * cos(phi), cos(theta),
sin(theta) * sin(phi)) + p;
r = max(length(z), 1e-6);
trap = min(trap, abs(z.y));
}
return 0.5 * log(r) * r / dr;
}
float bulbDist(float3 p, float power) {
float trap;
return bulb(p, power, trap);
}
// The surface's direction from four samples at a tetrahedron's corners.
float3 bulbNormal(float3 p, float power, float e) {
const float2 k = float2(1.0, -1.0);
return normalize(k.xyy * bulbDist(p + k.xyy * e, power)
+ k.yyx * bulbDist(p + k.yyx * e, power)
+ k.yxy * bulbDist(p + k.yxy * e, power)
+ k.xxx * bulbDist(p + k.xxx * e, power));
}
// Ambient occlusion: step out along the normal; where the bulb is nearer
// than the step, the room is shut in, and it darkens.
float occlusion(float3 p, float3 n, float power) {
float shut = 0.0;
float weight = 1.0;
for (int i = 1; i <= 5; i++) {
float h = 0.01 + 0.03 * float(i);
shut += (h - bulbDist(p + n * h, power)) * weight;
weight *= 0.75;
}
return clamp(1.0 - 3.0 * shut, 0.0, 1.0);
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * 0.5;
// The camera circles the bulb, rising and falling a little.
float yaw = t * 0.3;
float pitch = 0.35 + 0.25 * sin(t * 0.17);
float3 ro = 2.6 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(-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 q = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(q.x * rt + q.y * up + 2.0 * fw);
// One pixel's width a unit away: closer than a pixel is close enough.
float pixel = 1.0 / side;
float shift = (p.heat - 0.5) * 0.4;
float back = 0.06 + 0.10 * exp(-dot(q, q) * 0.6);
// March: step by the estimate until the ray is close enough.
float dist = 0.0;
bool hit = false;
float trap = 1.0;
for (int i = 0; i < 160; i++) {
float d = bulb(ro + rd * dist, p.power, trap);
if (d < pixel * dist) { hit = true; break; }
dist += d * 0.9;
if (dist > 6.0) break;
}
if (!hit) return float4(kiln(back + shift), 1.0);
float3 pos = ro + rd * dist;
float3 n = bulbNormal(pos, p.power, pixel * dist * 0.5);
float3 l = normalize(up * 0.8 - rt * 0.5 - fw * 0.4);
float lit = max(dot(n, l), 0.0);
float ao = occlusion(pos, n, p.power);
// The rim: edges turning away from the camera catch the kiln's glow.
float rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
float level = (0.35 + 0.65 * lit) * (0.4 + 0.6 * ao) + 0.12 * rim;
// Near the equator runs hot, far from it stays dark.
float orbit = 1.0 - sqrt(clamp(trap * 1.2, 0.0, 1.0));
float c = mix(0.15 + 0.75 * level,
0.1 + 0.6 * level + 0.6 * orbit * orbit, p.trap);
return float4(kiln(c + shift), 1.0);
}8 lines from stage 2 that are gone
float bulb(float3 p, float power) {
⋯
return normalize(k.xyy * bulb(p + k.xyy * e, power)
+ k.yyx * bulb(p + k.yyx * e, power)
+ k.yxy * bulb(p + k.yxy * e, power)
+ k.xxx * bulb(p + k.xxx * e, power));
⋯
shut += (h - bulb(p + n * h, power)) * weight;
⋯
float d = bulb(ro + rd * dist, p.power);
⋯
return float4(kiln(0.15 + 0.75 * level + shift), 1.0);Stage 4 · Mandelbulb
The last step lets it move and glow.
- The power breathes. It swings 1.5 times
breatheeither side ofpower, once every 31 seconds atspeed0.5, so the buds swell, split and fold back. - The glow. The march keeps the nearest the ray came to the bulb, and a ray that misses glows by
exp(-nearest * 18), so the bulb sits in a halo of its own heat. - A bounding sphere. The whole bulb is inside a sphere of radius 1.6, so the march starts where the ray enters it and gives up where it leaves; a ray that misses it isn't marched at all. Its edge is far enough out that the halo has faded there.
iterationssets how many times each point is raised to the power. At 1 the bulb is a smooth, wavy lump; at 2 it grows its buds; from 3 on, each more adds buds too small to see at this distance, but they are there when you zoom in.- The pointer. Its offset from the middle of the picture turns the camera further round the bulb and tips it up or down. In a video the pointer is in the middle, so it does nothing there; in the playground, move it over the picture, or click the picture and steer with the arrow keys.
the kiln palette and shade(), from the finished shader
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
// The power breathes slowly around its setting, so the buds swell and fold.
float power = max(p.power + p.breathe * 1.5 * sin(t * 0.4), 2.0);
int iterations = int(p.iterations + 0.5);
// The camera circles the bulb. The pointer's offset from the middle (zero in
// every video) turns it further round and tips it up or down.
float2 steer = u.mouse / u.resolution - 0.5;
float yaw = t * 0.3 + steer.x * TAU * 0.5;
float pitch = 0.35 + 0.25 * sin(t * 0.17) + steer.y * 2.0;
pitch = clamp(pitch, -1.4, 1.4);
float3 ro = 2.6 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(-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 q = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(q.x * rt + q.y * up + 2.0 * fw);
// How wide one pixel is a unit away: the march stops this close to the
// surface, so detail is as fine as the picture is sharp (a zoomed picture
// has more pixels, and finer detail).
float pixel = 1.0 / side;
float shift = (p.heat - 0.5) * 0.4;
// The kiln's dark behind it, warmer toward the middle of the picture.
float back = 0.06 + 0.10 * exp(-dot(q, q) * 0.6);
float2 span = bound(ro, rd);
if (span.y < span.x) return float4(kiln(back + shift), 1.0);
float dist = span.x;
float nearest = 1e3;
bool hit = false;
float trap = 1.0;
for (int i = 0; i < 160; i++) {
float3 pos = ro + rd * dist;
float d = bulb(pos, power, iterations, trap);
nearest = min(nearest, d);
if (d < pixel * dist) { hit = true; break; }
dist += d * 0.9;
if (dist > span.y) break;
}
if (!hit) {
// A ray that missed glows by how near it passed the bulb.
float halo = exp(-nearest * 18.0) * p.glow * 0.55;
return float4(kiln(back + halo + shift), 1.0);
}
float3 pos = ro + rd * dist;
float3 n = bulbNormal(pos, power, iterations, pixel * dist * 0.5);
// The light is above and to the left of the camera, and goes round with it.
float3 l = normalize(up * 0.8 - rt * 0.5 - fw * 0.4);
float lit = max(dot(n, l), 0.0);
float ao = occlusion(pos, n, power, iterations);
// The rim: edges facing away from the camera catch the kiln's glow.
float rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
float shadeLevel = (0.35 + 0.65 * lit) * (0.4 + 0.6 * ao)
+ 0.25 * rim * p.glow;
// The orbit trap: points whose orbit passed close to the equator run hot,
// the ones that kept away from it stay dark. Mixed with plain light and shade.
float orbit = 1.0 - sqrt(clamp(trap * 1.2, 0.0, 1.0));
float level = mix(0.15 + 0.75 * shadeLevel,
0.1 + 0.6 * shadeLevel + 0.6 * orbit * orbit, p.trap);
return float4(kiln(level + shift), 1.0);
}episode-20-mandelbulb.metal, the whole file68 new lines since stage 3
// Ray Kiln · Episode 20 · Mandelbulb
//
// @episode 20 Mandelbulb
// @length 60
// @variation 30
// @variations 1,2,3,6,7,8,9,11
// @sweeps power, breathe, iterations
// @still 21.0
// @teaches 3D fractals, distance estimators, orbit-trap colour
// @category Fragment
// @tags mandelbulb, fractals, distance estimators, orbit traps, raymarching, ambient occlusion, procedural sound
// @final Mandelbulb | The bulb glowing in the kiln's dark, its power breathing so the buds swell and fold, coloured by how near each point's orbit came to its equator.
//
// Episode 7 squared complex numbers over and over and coloured each point by whether it escaped. The Mandelbulb does
// the same in three dimensions: a point is turned into spherical coordinates, its distance raised to a power and its
// two angles multiplied by it, and the starting point added back. Points that never escape make the bulb. A ray can't
// step through that set by guessing, so the loop also keeps a running derivative, and from it a distance estimate:
// how far the ray can safely step before it could touch the surface. Colour comes from the orbit itself: how close
// each point's path came to the bulb's equator (its orbit trap).
// 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 camera circles and the power breathes
// @param power 8.0 3.0 12.0 The power each point is raised to: 3 is a folded lump, 8 the classic bulb, 12 a bristling one
// @param breathe 0.4 0.0 1.0 How far the power swells and shrinks around its setting as time goes on
// @param iterations 8.0 1.0 12.0 How many times each point is raised to the power: 1 is a smooth lump, 2 grows buds, 3 buds on them
// @param trap 0.7 0.0 1.0 How much the orbit trap colours the bulb, against plain light and shade
// @param glow 0.5 0.0 1.0 How brightly rays that pass near the bulb glow
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// ---- The bulb ---------------------------------------------------------------------------------------------------
// The distance estimate and orbit trap from stages 1 and 3, with up to 12
// turns: iterations is now a slider.
float bulb(float3 p, float power, int iterations, thread float& trap) {
float3 z = p;
float dr = 1.0;
float r = max(length(z), 1e-6);
trap = 1e3;
for (int i = 0; i < 12; i++) {
if (i >= iterations || r > 2.0) break;
float theta = acos(clamp(z.y / r, -1.0, 1.0)) * power;
float phi = atan2(z.z, z.x) * power;
dr = pow(r, power - 1.0) * power * dr + 1.0;
float zr = pow(r, power);
z = zr * float3(sin(theta) * cos(phi), cos(theta),
sin(theta) * sin(phi)) + p;
r = max(length(z), 1e-6);
trap = min(trap, abs(z.y));
}
return 0.5 * log(r) * r / dr;
}
float bulbDist(float3 p, float power, int iterations) {
float trap;
return bulb(p, power, iterations, trap);
}
// The normal from four samples, as before.
float3 bulbNormal(float3 p, float power, int iterations, float e) {
const float2 k = float2(1.0, -1.0);
return normalize(k.xyy * bulbDist(p + k.xyy * e, power, iterations)
+ k.yyx * bulbDist(p + k.yyx * e, power, iterations)
+ k.yxy * bulbDist(p + k.yxy * e, power, iterations)
+ k.xxx * bulbDist(p + k.xxx * e, power, iterations));
}
// Ambient occlusion, as in stage 2.
float occlusion(float3 p, float3 n, float power, int iterations) {
float shut = 0.0;
float weight = 1.0;
for (int i = 1; i <= 5; i++) {
float h = 0.01 + 0.03 * float(i);
shut += (h - bulbDist(p + n * h, power, iterations)) * weight;
weight *= 0.75;
}
return clamp(1.0 - 3.0 * shut, 0.0, 1.0);
}
// Where the ray enters and leaves a sphere of radius 1.6 around the bulb.
// Nothing outside it is marched.
float2 bound(float3 ro, float3 rd) {
float b = dot(ro, rd);
float c = dot(ro, ro) - 1.6 * 1.6;
float h = b * b - c;
if (h < 0.0) return float2(1.0, -1.0);
h = sqrt(h);
return float2(max(-b - h, 0.0), -b + h);
}
// ---- The picture -------------------------------------------------------------------------------------------------
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
// The power breathes slowly around its setting, so the buds swell and fold.
float power = max(p.power + p.breathe * 1.5 * sin(t * 0.4), 2.0);
int iterations = int(p.iterations + 0.5);
// The camera circles the bulb. The pointer's offset from the middle (zero in
// every video) turns it further round and tips it up or down.
float2 steer = u.mouse / u.resolution - 0.5;
float yaw = t * 0.3 + steer.x * TAU * 0.5;
float pitch = 0.35 + 0.25 * sin(t * 0.17) + steer.y * 2.0;
pitch = clamp(pitch, -1.4, 1.4);
float3 ro = 2.6 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(-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 q = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(q.x * rt + q.y * up + 2.0 * fw);
// How wide one pixel is a unit away: the march stops this close to the
// surface, so detail is as fine as the picture is sharp (a zoomed picture
// has more pixels, and finer detail).
float pixel = 1.0 / side;
float shift = (p.heat - 0.5) * 0.4;
// The kiln's dark behind it, warmer toward the middle of the picture.
float back = 0.06 + 0.10 * exp(-dot(q, q) * 0.6);
float2 span = bound(ro, rd);
if (span.y < span.x) return float4(kiln(back + shift), 1.0);
float dist = span.x;
float nearest = 1e3;
bool hit = false;
float trap = 1.0;
for (int i = 0; i < 160; i++) {
float3 pos = ro + rd * dist;
float d = bulb(pos, power, iterations, trap);
nearest = min(nearest, d);
if (d < pixel * dist) { hit = true; break; }
dist += d * 0.9;
if (dist > span.y) break;
}
if (!hit) {
// A ray that missed glows by how near it passed the bulb.
float halo = exp(-nearest * 18.0) * p.glow * 0.55;
return float4(kiln(back + halo + shift), 1.0);
}
float3 pos = ro + rd * dist;
float3 n = bulbNormal(pos, power, iterations, pixel * dist * 0.5);
// The light is above and to the left of the camera, and goes round with it.
float3 l = normalize(up * 0.8 - rt * 0.5 - fw * 0.4);
float lit = max(dot(n, l), 0.0);
float ao = occlusion(pos, n, power, iterations);
// The rim: edges facing away from the camera catch the kiln's glow.
float rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
float shadeLevel = (0.35 + 0.65 * lit) * (0.4 + 0.6 * ao)
+ 0.25 * rim * p.glow;
// The orbit trap: points whose orbit passed close to the equator run hot,
// the ones that kept away from it stay dark. Mixed with plain light and shade.
float orbit = 1.0 - sqrt(clamp(trap * 1.2, 0.0, 1.0));
float level = mix(0.15 + 0.75 * shadeLevel,
0.1 + 0.6 * shadeLevel + 0.6 * orbit * orbit, p.trap);
return float4(kiln(level + shift), 1.0);
}
// ---- Sound -------------------------------------------------------------------------------------------------------
// A fractal looks the same however far in you go; this sound does the same in pitch. Six tones an octave apart glide
// up together, each fading in at the bottom and out at the top (a Shepard-Risset glide), so the whole seems to rise
// forever without going anywhere. A second glide a fifth above follows the orbit trap's colour, and the breathing
// power swells both.
float glide(float t, float rate, float base, float pan) {
float a = 0.0;
for (int k = 0; k < 6; k++) {
// x runs 0 to 1 as this tone climbs six octaves, then starts again.
float x = fract(t * rate + float(k) / 6.0);
// The phase is the frequency's integral: base * 2^(6x) / (6 * rate * ln 2).
float phase = base * exp2(6.0 * x) / (6.0 * rate * 0.6931472);
float env = sin(3.14159265 * x);
a += sin(TAU * fract(phase) + pan * float(k)) * env * env;
}
return a / 3.0;
}
float2 sound(float t, constant Params& p) {
float rate = 0.004 + 0.01 * p.speed;
float swell = 0.8 + 0.2 * sin(t * p.speed * 0.4) * p.breathe;
float2 low = float2(glide(t, rate, 55.0, 0.0),
glide(t + 0.37, rate, 55.0, 0.9));
float2 fifth = float2(glide(t + 0.21, rate, 82.41, 0.4),
glide(t, rate, 82.41, 1.3));
float2 mixed = low + fifth * 0.6 * p.trap;
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(mixed * 0.5 * swell) * 0.8 * fadeIn * p.volume;
}25 lines from stage 3 that are gone
float bulb(float3 p, float power, thread float& trap) {
⋯
for (int i = 0; i < 8; i++) {
if (r > 2.0) break;
⋯
float bulbDist(float3 p, float power) {
⋯
return bulb(p, power, trap);
⋯
float3 bulbNormal(float3 p, float power, float e) {
⋯
return normalize(k.xyy * bulbDist(p + k.xyy * e, power)
+ k.yyx * bulbDist(p + k.yyx * e, power)
+ k.yxy * bulbDist(p + k.yxy * e, power)
+ k.xxx * bulbDist(p + k.xxx * e, power));
⋯
float occlusion(float3 p, float3 n, float power) {
⋯
shut += (h - bulbDist(p + n * h, power)) * weight;
⋯
float t = u.time * 0.5;
⋯
float yaw = t * 0.3;
float pitch = 0.35 + 0.25 * sin(t * 0.17);
⋯
float dist = 0.0;
⋯
float d = bulb(ro + rd * dist, p.power, trap);
⋯
if (dist > 6.0) break;
⋯
if (!hit) return float4(kiln(back + shift), 1.0);
⋯
float3 n = bulbNormal(pos, p.power, pixel * dist * 0.5);
⋯
float ao = occlusion(pos, n, p.power);
⋯
float level = (0.35 + 0.65 * lit) * (0.4 + 0.6 * ao) + 0.12 * rim;
⋯
float c = mix(0.15 + 0.75 * level,
0.1 + 0.6 * level + 0.6 * orbit * orbit, p.trap);
return float4(kiln(c + shift), 1.0);The sound
A fractal looks alike however far in you go, and the sound does that in pitch. glide plays six tones an octave apart, all climbing together; each fades in at the bottom of its six octaves and out at the top, then starts again at the bottom, so the sound seems to rise forever without getting anywhere (a Shepard-Risset glide). Each tone's phase is worked out as the integral of its rising frequency, base * 2^(6x) / (6 * rate * ln 2), so it glides without a click.
There are two glides, one from 55 Hz and one a fifth above from 82.4 Hz, as loud as trap sets it. They climb a little faster with speed, swell with the breathing of the power, go through tanh, and fade in over the first two seconds. At the defaults the peak is about -9.8 dBFS and the average (RMS) about -20.4 dBFS.
New in Ray Kiln
Moment links. The playground's Share link now holds the moment as well as the code: the clock (and whether it was paused), every slider, the zoom and where the pointer was. Whoever opens the link sees the shader at that moment, from that angle, rather than starting again at 0. All of it goes after the # with the code, so it never reaches a server and nothing is stored, and a link made before this still opens as it did.
A fractal is where this is most useful. Open the Mandelbulb in the playground, move the pointer to turn the camera, pause on a bud you like, press Zoom and wheel in on it, then press Share link: the link opens on that bud, paused, with your power and trap. Because the march stops at a pixel's width, a zoomed picture has more pixels, and the bulb shows finer detail the further in you go.
Try this
- Set
powerto 3, then to 12: a folded, lumpy shape, then a rounder ball covered in rows of smaller buds. - Set
breatheto 1 andspeedto 2: the buds swell and split as you watch. - Set
trapto 0 for plain light and shade, then to 1: the veins of the orbit trap are all that colours it. - Set
iterationsto 1, then 2, then 3: a smooth lump, then buds, then buds on the buds. - In the playground, press Zoom and wheel in on a bud, then set
iterationsto 3 and back to 12: up close, the extra turns are what make the buds on the buds. - Set
glowto 0: the halo goes, and so does the glow on the bulb's rim. - In
bulb, changetrap = min(trap, abs(z.y));totrap = min(trap, length(z.xz));: the trap is now the bulb's axis instead of its equator; few orbits come near it, and the veins all but vanish. - In
shade, changeif (d < pixel * dist)toif (d < 0.01): the march stops further from the surface, and the finest buds melt away.
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 camera circles and the power breathes |
power | 8.0 | 3.0 to 12.0 | The power each point is raised to: 3 is a folded lump, 8 the classic bulb, 12 a bristling one |
breathe | 0.4 | 0.0 to 1.0 | How far the power swells and shrinks around its setting as time goes on |
iterations | 8.0 | 1.0 to 12.0 | How many times each point is raised to the power: 1 is a smooth lump, 2 grows buds, 3 buds on them |
trap | 0.7 | 0.0 to 1.0 | How much the orbit trap colours the bulb, against plain light and shade |
glow | 0.5 | 0.0 to 1.0 | How brightly rays that pass near the bulb glow |
heat | 0.5 | 0.0 to 1.0 | Slides the palette from deep ember to white-hot |
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-20-mandelbulb.metal
// Ray Kiln · Episode 20 · Mandelbulb
//
// @episode 20 Mandelbulb
// @length 60
// @variation 30
// @variations 1,2,3,6,7,8,9,11
// @sweeps power, breathe, iterations
// @still 21.0
// @teaches 3D fractals, distance estimators, orbit-trap colour
// @category Fragment
// @tags mandelbulb, fractals, distance estimators, orbit traps, raymarching, ambient occlusion, procedural sound
// @final Mandelbulb | The bulb glowing in the kiln's dark, its power breathing so the buds swell and fold, coloured by how near each point's orbit came to its equator.
//
// Episode 7 squared complex numbers over and over and coloured each point by whether it escaped. The Mandelbulb does
// the same in three dimensions: a point is turned into spherical coordinates, its distance raised to a power and its
// two angles multiplied by it, and the starting point added back. Points that never escape make the bulb. A ray can't
// step through that set by guessing, so the loop also keeps a running derivative, and from it a distance estimate:
// how far the ray can safely step before it could touch the surface. Colour comes from the orbit itself: how close
// each point's path came to the bulb's equator (its orbit trap).
// 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 camera circles and the power breathes
// @param power 8.0 3.0 12.0 The power each point is raised to: 3 is a folded lump, 8 the classic bulb, 12 a bristling one
// @param breathe 0.4 0.0 1.0 How far the power swells and shrinks around its setting as time goes on
// @param iterations 8.0 1.0 12.0 How many times each point is raised to the power: 1 is a smooth lump, 2 grows buds, 3 buds on them
// @param trap 0.7 0.0 1.0 How much the orbit trap colours the bulb, against plain light and shade
// @param glow 0.5 0.0 1.0 How brightly rays that pass near the bulb glow
// @param heat 0.5 0.0 1.0 Slides the palette from deep ember to white-hot
// @param volume 0.8 0.0 1.0 Loudness of the sound
constant float TAU = 6.2831853;
// The kiln palette from episode 1.
float3 kiln(float t) {
float3 c0 = float3(0.020, 0.012, 0.020);
float3 c1 = float3(0.280, 0.040, 0.030);
float3 c2 = float3(0.880, 0.260, 0.050);
float3 c3 = float3(1.000, 0.680, 0.200);
float3 c4 = float3(1.000, 0.970, 0.840);
t = clamp(t, 0.0, 1.0);
float3 c = mix(c0, c1, smoothstep(0.00, 0.22, t));
c = mix(c, c2, smoothstep(0.22, 0.50, t));
c = mix(c, c3, smoothstep(0.50, 0.76, t));
return mix(c, c4, smoothstep(0.76, 1.00, t));
}
// ---- The bulb ---------------------------------------------------------------------------------------------------
// The distance estimate and orbit trap from stages 1 and 3, with up to 12
// turns: iterations is now a slider.
float bulb(float3 p, float power, int iterations, thread float& trap) {
float3 z = p;
float dr = 1.0;
float r = max(length(z), 1e-6);
trap = 1e3;
for (int i = 0; i < 12; i++) {
if (i >= iterations || r > 2.0) break;
float theta = acos(clamp(z.y / r, -1.0, 1.0)) * power;
float phi = atan2(z.z, z.x) * power;
dr = pow(r, power - 1.0) * power * dr + 1.0;
float zr = pow(r, power);
z = zr * float3(sin(theta) * cos(phi), cos(theta),
sin(theta) * sin(phi)) + p;
r = max(length(z), 1e-6);
trap = min(trap, abs(z.y));
}
return 0.5 * log(r) * r / dr;
}
float bulbDist(float3 p, float power, int iterations) {
float trap;
return bulb(p, power, iterations, trap);
}
// The normal from four samples, as before.
float3 bulbNormal(float3 p, float power, int iterations, float e) {
const float2 k = float2(1.0, -1.0);
return normalize(k.xyy * bulbDist(p + k.xyy * e, power, iterations)
+ k.yyx * bulbDist(p + k.yyx * e, power, iterations)
+ k.yxy * bulbDist(p + k.yxy * e, power, iterations)
+ k.xxx * bulbDist(p + k.xxx * e, power, iterations));
}
// Ambient occlusion, as in stage 2.
float occlusion(float3 p, float3 n, float power, int iterations) {
float shut = 0.0;
float weight = 1.0;
for (int i = 1; i <= 5; i++) {
float h = 0.01 + 0.03 * float(i);
shut += (h - bulbDist(p + n * h, power, iterations)) * weight;
weight *= 0.75;
}
return clamp(1.0 - 3.0 * shut, 0.0, 1.0);
}
// Where the ray enters and leaves a sphere of radius 1.6 around the bulb.
// Nothing outside it is marched.
float2 bound(float3 ro, float3 rd) {
float b = dot(ro, rd);
float c = dot(ro, ro) - 1.6 * 1.6;
float h = b * b - c;
if (h < 0.0) return float2(1.0, -1.0);
h = sqrt(h);
return float2(max(-b - h, 0.0), -b + h);
}
// ---- The picture -------------------------------------------------------------------------------------------------
float4 shade(float2 uv, constant Uniforms& u, constant Params& p) {
float t = u.time * p.speed;
// The power breathes slowly around its setting, so the buds swell and fold.
float power = max(p.power + p.breathe * 1.5 * sin(t * 0.4), 2.0);
int iterations = int(p.iterations + 0.5);
// The camera circles the bulb. The pointer's offset from the middle (zero in
// every video) turns it further round and tips it up or down.
float2 steer = u.mouse / u.resolution - 0.5;
float yaw = t * 0.3 + steer.x * TAU * 0.5;
float pitch = 0.35 + 0.25 * sin(t * 0.17) + steer.y * 2.0;
pitch = clamp(pitch, -1.4, 1.4);
float3 ro = 2.6 * float3(cos(pitch) * sin(yaw), sin(pitch),
cos(pitch) * cos(yaw));
float3 fw = normalize(-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 q = (uv - 0.5) * u.resolution / side * 2.0;
float3 rd = normalize(q.x * rt + q.y * up + 2.0 * fw);
// How wide one pixel is a unit away: the march stops this close to the
// surface, so detail is as fine as the picture is sharp (a zoomed picture
// has more pixels, and finer detail).
float pixel = 1.0 / side;
float shift = (p.heat - 0.5) * 0.4;
// The kiln's dark behind it, warmer toward the middle of the picture.
float back = 0.06 + 0.10 * exp(-dot(q, q) * 0.6);
float2 span = bound(ro, rd);
if (span.y < span.x) return float4(kiln(back + shift), 1.0);
float dist = span.x;
float nearest = 1e3;
bool hit = false;
float trap = 1.0;
for (int i = 0; i < 160; i++) {
float3 pos = ro + rd * dist;
float d = bulb(pos, power, iterations, trap);
nearest = min(nearest, d);
if (d < pixel * dist) { hit = true; break; }
dist += d * 0.9;
if (dist > span.y) break;
}
if (!hit) {
// A ray that missed glows by how near it passed the bulb.
float halo = exp(-nearest * 18.0) * p.glow * 0.55;
return float4(kiln(back + halo + shift), 1.0);
}
float3 pos = ro + rd * dist;
float3 n = bulbNormal(pos, power, iterations, pixel * dist * 0.5);
// The light is above and to the left of the camera, and goes round with it.
float3 l = normalize(up * 0.8 - rt * 0.5 - fw * 0.4);
float lit = max(dot(n, l), 0.0);
float ao = occlusion(pos, n, power, iterations);
// The rim: edges facing away from the camera catch the kiln's glow.
float rim = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
float shadeLevel = (0.35 + 0.65 * lit) * (0.4 + 0.6 * ao)
+ 0.25 * rim * p.glow;
// The orbit trap: points whose orbit passed close to the equator run hot,
// the ones that kept away from it stay dark. Mixed with plain light and shade.
float orbit = 1.0 - sqrt(clamp(trap * 1.2, 0.0, 1.0));
float level = mix(0.15 + 0.75 * shadeLevel,
0.1 + 0.6 * shadeLevel + 0.6 * orbit * orbit, p.trap);
return float4(kiln(level + shift), 1.0);
}
// ---- Sound -------------------------------------------------------------------------------------------------------
// A fractal looks the same however far in you go; this sound does the same in pitch. Six tones an octave apart glide
// up together, each fading in at the bottom and out at the top (a Shepard-Risset glide), so the whole seems to rise
// forever without going anywhere. A second glide a fifth above follows the orbit trap's colour, and the breathing
// power swells both.
float glide(float t, float rate, float base, float pan) {
float a = 0.0;
for (int k = 0; k < 6; k++) {
// x runs 0 to 1 as this tone climbs six octaves, then starts again.
float x = fract(t * rate + float(k) / 6.0);
// The phase is the frequency's integral: base * 2^(6x) / (6 * rate * ln 2).
float phase = base * exp2(6.0 * x) / (6.0 * rate * 0.6931472);
float env = sin(3.14159265 * x);
a += sin(TAU * fract(phase) + pan * float(k)) * env * env;
}
return a / 3.0;
}
float2 sound(float t, constant Params& p) {
float rate = 0.004 + 0.01 * p.speed;
float swell = 0.8 + 0.2 * sin(t * p.speed * 0.4) * p.breathe;
float2 low = float2(glide(t, rate, 55.0, 0.0),
glide(t + 0.37, rate, 55.0, 0.9));
float2 fifth = float2(glide(t + 0.21, rate, 82.41, 0.4),
glide(t, rate, 82.41, 1.3));
float2 mixed = low + fifth * 0.6 * p.trap;
float fadeIn = smoothstep(0.0, 2.0, t);
return tanh(mixed * 0.5 * swell) * 0.8 * 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, 8.0, 0.4, 8.0, 0.7, 0.5, 0.5, 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 Fractals
Press ? for keyboard shortcuts.
