Library
The demo library
Every demo on this site is one fragment shader plus a few shared pieces. A demo pulls a piece in with a line like#include "sdf2d". Here is all of it.
Demos are written in GLSL for WebGL 2. The Unity shaders they explain are written in HLSL; the two languages are close, and the common piece adds HLSL names like saturate and lerp so the demo code reads the same.
How a demo is put together
A demo defines mainImage, which runs once for every pixel and writes its color. The site adds these inputs in front of it:
uResolution: the canvas size in pixels.uTime: seconds since the demo started. Only demos that read it are animated.uPointer: where the reader last dragged or pointed, from 0 to 1.uDark: 1 on the dark theme, so diagrams can match the page.
Sliders are uniforms with a note behind them, for exampleuniform float uDepth; // @range 0 1 0.01 = 0.4 | Depth.
common
Always included. Constants, HLSL names like saturate and lerp, centered coordinates, anti-aliasing, color helpers and the site colors.
Show common.glsl
// Always included. Constants, HLSL-style helpers, coordinates, anti-aliasing and theme colors.
#define PI 3.14159265359
#define TAU 6.28318530718
// HLSL names, so demo code reads like the Unity shaders it explains.
#define lerp mix
#define frac fract
float saturate(float x) { return clamp(x, 0.0, 1.0); }
vec2 saturate(vec2 x) { return clamp(x, 0.0, 1.0); }
vec3 saturate(vec3 x) { return clamp(x, 0.0, 1.0); }
vec4 saturate(vec4 x) { return clamp(x, 0.0, 1.0); }
// smoothstep that also accepts e0 > e1 (a falling edge), as HLSL does. GLSL leaves that case undefined.
float sstep(float e0, float e1, float x) {
float t = saturate((x - e0) / (e1 - e0));
return t * t * (3.0 - 2.0 * t);
}
float remap(float x, float a, float b, float c, float d) { return c + (x - a) * (d - c) / (b - a); }
float remap01(float x, float a, float b) { return saturate((x - a) / (b - a)); }
// Square coordinates with the origin in the middle: the short side of the canvas spans -1..1.
vec2 centered(vec2 fragCoord) {
return (2.0 * fragCoord - uResolution) / min(uResolution.x, uResolution.y);
}
// The pointer in the same coordinates as centered().
vec2 pointerCentered() {
return (2.0 * uPointer * uResolution - uResolution) / min(uResolution.x, uResolution.y);
}
// Width of one pixel in centered() units.
float pixelSize() { return 2.0 / min(uResolution.x, uResolution.y); }
// Anti-aliased coverage of a signed distance: 1 inside, 0 outside, a one pixel ramp at the edge.
float fillAA(float d) {
float w = max(fwidth(d), 1e-5);
return saturate(0.5 - d / w);
}
// Anti-aliased line of a given width (same units as d) around the zero line of d.
float strokeAA(float d, float width) {
return fillAA(abs(d) - 0.5 * width);
}
// Cheap hashes: one random number per input.
float hash11(float p) {
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash12(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
mat2 rot2(float a) {
float c = cos(a), s = sin(a);
return mat2(c, s, -s, c);
}
// Color. Effect demos work in linear light and convert once at the end, like Unity in linear space.
vec3 toSrgb(vec3 c) {
c = max(c, 0.0);
return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));
}
vec3 fromSrgb(vec3 c) {
return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));
}
// Rolls HDR values above 1 off softly instead of clipping them.
vec3 tonemapACES(vec3 x) {
return saturate((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14));
}
float luminance(vec3 c) { return dot(c, vec3(0.2126, 0.7152, 0.0722)); }
// Stage colors in sRGB, for plots and diagrams. Demos sit on the dusk stage (uDark = 1);
// the paper values (uDark = 0) are kept for a demo that wants a light stage.
vec3 themeSurface() { return mix(vec3(0.984, 0.973, 1.000), vec3(0.086, 0.059, 0.169), uDark); }
vec3 themeInk() { return mix(vec3(0.129, 0.094, 0.271), vec3(0.945, 0.914, 1.000), uDark); }
vec3 themeMuted() { return mix(vec3(0.361, 0.322, 0.502), vec3(0.663, 0.608, 0.820), uDark); }
vec3 themeGrid() { return mix(vec3(0.914, 0.882, 0.969), vec3(0.169, 0.122, 0.282), uDark); }
// Series colors, used in this order: mint, pink, butter. Plots also tell them apart by line style.
vec3 series1() { return mix(vec3(0.039, 0.459, 0.345), vec3(0.494, 0.941, 0.784), uDark); }
vec3 series2() { return mix(vec3(0.722, 0.094, 0.435), vec3(1.000, 0.561, 0.784), uDark); }
vec3 series3() { return mix(vec3(0.592, 0.282, 0.039), vec3(1.000, 0.831, 0.541), uDark); }sdf2d
Signed distance functions for 2D shapes and the ways to combine them.
Show sdf2d.glsl
// 2D signed distance functions. Negative inside, positive outside, 0 on the edge.
// After Inigo Quilez, https://iquilezles.org/articles/distfunctions2d/ (MIT License).
#ifndef DOCME_SDF2D
#define DOCME_SDF2D
float dot2(vec2 v) { return dot(v, v); }
float sdCircle(vec2 p, float r) {
return length(p) - r;
}
float sdBox(vec2 p, vec2 halfSize) {
vec2 d = abs(p) - halfSize;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}
float sdRoundedBox(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + r;
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
}
float sdSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
// Fast approximation; good near the edge, which is all anti-aliasing needs.
float sdEllipse(vec2 p, vec2 r) {
float k0 = length(p / r);
float k1 = length(p / (r * r));
return k0 * (k0 - 1.0) / k1;
}
// Lens shape: the overlap of two circles of radius r centred at x = -d and x = +d.
float sdVesica(vec2 p, float r, float d) {
p = abs(p);
float b = sqrt(r * r - d * d);
return ((p.y - b) * d > p.x * b) ? length(p - vec2(0.0, b)) : length(p - vec2(-d, 0.0)) - r;
}
// Circle of radius ra with a circle of radius rb, d to the right, cut out of it.
float sdMoon(vec2 p, float d, float ra, float rb) {
p.y = abs(p.y);
float a = (ra * ra - rb * rb + d * d) / (2.0 * d);
float b = sqrt(max(ra * ra - a * a, 0.0));
if (d * (p.x * b - p.y * a) > d * d * max(b - p.y, 0.0)) return length(p - vec2(a, b));
return max(length(p) - ra, -(length(p - vec2(d, 0.0)) - rb));
}
// Arc around the y axis. sc = vec2(sin, cos) of half the opening angle; ra radius, rb thickness.
float sdArc(vec2 p, vec2 sc, float ra, float rb) {
p.x = abs(p.x);
return ((sc.y * p.x > sc.x * p.y) ? length(p - sc * ra) : abs(length(p) - ra)) - rb;
}
float sdRing(vec2 p, float r, float thickness) {
return abs(length(p) - r) - 0.5 * thickness;
}
float sdEquilateralTriangle(vec2 p, float r) {
const float k = 1.7320508;
p.x = abs(p.x) - r;
p.y = p.y + r / k;
if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
p.x -= clamp(p.x, -2.0 * r, 0.0);
return -length(p) * sign(p.y);
}
float sdStar5(vec2 p, float r, float rf) {
const vec2 k1 = vec2(0.809016994375, -0.587785252292);
const vec2 k2 = vec2(-k1.x, k1.y);
p.x = abs(p.x);
p -= 2.0 * max(dot(k1, p), 0.0) * k1;
p -= 2.0 * max(dot(k2, p), 0.0) * k2;
p.x = abs(p.x);
p.y -= r;
vec2 ba = rf * vec2(-k1.y, k1.x) - vec2(0.0, 1.0);
float h = clamp(dot(p, ba) / dot(ba, ba), 0.0, r);
return length(p - ba * h) * sign(p.y * ba.x - p.x * ba.y);
}
// Heart of height about 1, its tip at the origin.
float sdHeart(vec2 p) {
p.x = abs(p.x);
if (p.y + p.x > 1.0) return sqrt(dot2(p - vec2(0.25, 0.75))) - sqrt(2.0) / 4.0;
return sqrt(min(dot2(p - vec2(0.0, 1.0)), dot2(p - 0.5 * max(p.x + p.y, 0.0)))) * sign(p.x - p.y);
}
float sdHexagon(vec2 p, float r) {
const vec3 k = vec3(-0.866025404, 0.5, 0.577350269);
p = abs(p);
p -= 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
return length(p) * sign(p.y);
}
// Combining shapes.
float opUnion(float a, float b) { return min(a, b); }
// opSubtract(a, b) cuts a out of b, in the same argument order as Inigo Quilez's opSubtraction.
float opSubtract(float a, float b) { return max(-a, b); }
float opIntersect(float a, float b) { return max(a, b); }
// Blends two shapes over a distance k instead of a sharp crease.
float opSmoothUnion(float a, float b, float k) {
float h = saturate(0.5 + 0.5 * (b - a) / k);
return mix(b, a, h) - k * h * (1.0 - h);
}
// Cuts a out of b with a rounded edge of size k.
float opSmoothSubtract(float a, float b, float k) { return -opSmoothUnion(a, -b, k); }
float opRound(float d, float r) { return d - r; }
// A ring around the edge, halfWidth on each side.
float opOnion(float d, float halfWidth) { return abs(d) - halfWidth; }
#endifnoise
Value, gradient, fractal and cellular noise.
Show noise.glsl
// Noise. Value and gradient noise after Inigo Quilez (MIT License).
#ifndef DOCME_NOISE
#define DOCME_NOISE
// Value noise: random heights on a grid, smoothly blended. Range 0..1.
float valueNoise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash12(i), b = hash12(i + vec2(1.0, 0.0));
float c = hash12(i + vec2(0.0, 1.0)), d = hash12(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// Gradient noise: random slopes on a grid. Smoother looking than value noise. Range about -1..1.
float gradientNoise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
vec2 ga = hash22(i) * 2.0 - 1.0;
vec2 gb = hash22(i + vec2(1.0, 0.0)) * 2.0 - 1.0;
vec2 gc = hash22(i + vec2(0.0, 1.0)) * 2.0 - 1.0;
vec2 gd = hash22(i + vec2(1.0, 1.0)) * 2.0 - 1.0;
float va = dot(ga, f), vb = dot(gb, f - vec2(1.0, 0.0));
float vc = dot(gc, f - vec2(0.0, 1.0)), vd = dot(gd, f - vec2(1.0, 1.0));
return 1.4 * mix(mix(va, vb, u.x), mix(vc, vd, u.x), u.y);
}
// Fractal noise: octaves of gradient noise, each twice as fine and half as strong. Range about -1..1.
float fbm(vec2 p, int octaves) {
float sum = 0.0, amp = 0.5;
mat2 m = mat2(1.6, 1.2, -1.2, 1.6);
for (int i = 0; i < 8; i++) {
if (i >= octaves) break;
sum += amp * gradientNoise(p);
p = m * p;
amp *= 0.5;
}
return sum;
}
// 3D value noise, for smoke and other things that change over time.
float hash13(vec3 p3) {
p3 = fract(p3 * 0.1031);
p3 += dot(p3, p3.zyx + 31.32);
return fract((p3.x + p3.y) * p3.z);
}
float valueNoise3(vec3 p) {
vec3 i = floor(p), f = fract(p);
vec3 u = f * f * (3.0 - 2.0 * f);
return mix(
mix(mix(hash13(i), hash13(i + vec3(1, 0, 0)), u.x),
mix(hash13(i + vec3(0, 1, 0)), hash13(i + vec3(1, 1, 0)), u.x), u.y),
mix(mix(hash13(i + vec3(0, 0, 1)), hash13(i + vec3(1, 0, 1)), u.x),
mix(hash13(i + vec3(0, 1, 1)), hash13(i + vec3(1, 1, 1)), u.x), u.y), u.z);
}
// Cellular (Voronoi) noise. x: distance to the nearest random point, yz: that cell's id.
vec3 voronoi(vec2 p) {
vec2 i = floor(p), f = fract(p);
float best = 8.0;
vec2 id = vec2(0.0);
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++) {
vec2 g = vec2(float(x), float(y));
vec2 o = hash22(i + g);
float d = length(g + o - f);
if (d < best) { best = d; id = i + g; }
}
return vec3(best, id);
}
#endifplot
Draws function graphs. The Plot component writes the code that calls it.
Show plot.glsl
// Function plots. Plot.astro defines PLOT_X_RANGE, PLOT_Y_RANGE and PLOT_TICK before including this.
#ifndef DOCME_PLOT
#define DOCME_PLOT
vec2 plotCoord(vec2 fragCoord) {
vec2 t = fragCoord / uResolution;
return vec2(mix(PLOT_X_RANGE.x, PLOT_X_RANGE.y, t.x), mix(PLOT_Y_RANGE.x, PLOT_Y_RANGE.y, t.y));
}
// Size of one pixel in data units.
vec2 plotPixel() {
return vec2(PLOT_X_RANGE.y - PLOT_X_RANGE.x, PLOT_Y_RANGE.y - PLOT_Y_RANGE.x) / uResolution;
}
vec3 plotBackground(vec2 p) {
vec2 px = plotPixel();
vec3 col = themeSurface();
vec2 g = abs(p - PLOT_TICK * round(p / PLOT_TICK)) / px;
col = mix(col, themeGrid(), saturate(1.0 - min(g.x, g.y)));
vec2 axis = abs(p) / px;
col = mix(col, themeMuted(), 0.8 * saturate(1.25 - min(axis.x, axis.y)));
return col;
}
// f'(x) from f one pixel before, at and after x. Where f jumps (a step, a saw tooth), one side changes by many
// pixels and the other hardly at all: then the gentler side wins, so the line ends on each side of the jump
// instead of smearing up and down through it.
float plotSlope(float before, float here, float after, float h) {
float a = here - before;
float b = after - here;
float big = max(abs(a), abs(b));
if (big > 8.0 * plotPixel().y && big > 4.0 * min(abs(a), abs(b))) return (abs(a) < abs(b) ? a : b) / h;
return 0.5 * (a + b) / h;
}
// Draws y = f(x) as a 2.5 px line. slope is f'(x), so the line keeps its width where it is steep.
vec3 plotCurve(vec3 col, vec2 p, float y, float slope, vec3 ink) {
vec2 px = plotPixel();
float k = slope * px.x / px.y;
float d = abs(p.y - y) / px.y / sqrt(1.0 + k * k);
return mix(col, ink, saturate(1.75 - d));
}
// The same line, solid (style 0), dashed (1) or dotted (2), so curves differ by more than color.
vec3 plotCurveStyled(vec3 col, vec2 p, float y, float slope, vec3 ink, int style) {
float on = 1.0;
if (style == 1) on = step(fract(gl_FragCoord.x / 14.0), 0.62);
if (style == 2) on = step(fract(gl_FragCoord.x / 7.0), 0.5);
vec2 px = plotPixel();
float k = slope * px.x / px.y;
float d = abs(p.y - y) / px.y / sqrt(1.0 + k * k);
return mix(col, ink, on * saturate(1.75 - d));
}
// Shades the area between y = f(x) and y = 0.
vec3 plotArea(vec3 col, vec2 p, float y, vec3 ink, float alpha) {
float inside = step(min(0.0, y), p.y) * step(p.y, max(0.0, y));
return mix(col, ink, alpha * inside);
}
// Dashed vertical guide at x.
vec3 plotGuideX(vec3 col, vec2 p, float x, vec3 ink) {
float d = abs(p.x - x) / plotPixel().x;
float dash = step(0.5, fract(gl_FragCoord.y / 10.0));
return mix(col, ink, 0.9 * dash * saturate(1.25 - d));
}
// Dashed horizontal guide at y.
vec3 plotGuideY(vec3 col, vec2 p, float y, vec3 ink) {
float d = abs(p.y - y) / plotPixel().y;
float dash = step(0.5, fract(gl_FragCoord.x / 10.0));
return mix(col, ink, 0.9 * dash * saturate(1.25 - d));
}
// A 5 px dot with a 2 px ring in the surface color, so it stays readable on top of a line.
vec3 plotPoint(vec3 col, vec2 p, vec2 at, vec3 ink) {
float d = length((p - at) / plotPixel());
col = mix(col, themeSurface(), saturate(7.5 - d));
return mix(col, ink, saturate(5.5 - d));
}
#endifdiagram
Lines, arrows, dots and distance field views for geometry diagrams.
Show diagram.glsl
// Geometry diagrams in centered() coordinates. Widths are in pixels, so lines look the same at any size.
#ifndef DOCME_DIAGRAM
#define DOCME_DIAGRAM
float dgSegment(vec2 p, vec2 a, vec2 b) {
vec2 pa = p - a, ba = b - a;
float h = saturate(dot(pa, ba) / dot(ba, ba));
return length(pa - ba * h);
}
vec3 drawGrid(vec3 col, vec2 p, float spacing) {
vec2 g = abs(p - spacing * round(p / spacing)) / pixelSize();
return mix(col, themeGrid(), saturate(1.0 - min(g.x, g.y)));
}
vec3 drawAxes(vec3 col, vec2 p) {
vec2 a = abs(p) / pixelSize();
return mix(col, themeMuted(), 0.8 * saturate(1.25 - min(a.x, a.y)));
}
vec3 drawLine(vec3 col, vec2 p, vec2 a, vec2 b, float widthPx, vec3 ink) {
float d = dgSegment(p, a, b) / pixelSize();
return mix(col, ink, saturate(0.5 * widthPx + 0.5 - d));
}
vec3 drawDashed(vec3 col, vec2 p, vec2 a, vec2 b, float widthPx, vec3 ink) {
vec2 ba = b - a;
float along = dot(p - a, ba) / length(ba) / pixelSize();
float dash = step(0.45, fract(along / 12.0));
float d = dgSegment(p, a, b) / pixelSize();
return mix(col, ink, dash * saturate(0.5 * widthPx + 0.5 - d));
}
vec3 drawArrow(vec3 col, vec2 p, vec2 a, vec2 b, float widthPx, vec3 ink) {
vec2 dir = normalize(b - a);
vec2 side = vec2(-dir.y, dir.x);
float head = 10.0 * pixelSize() + 1.5 * widthPx * pixelSize();
vec2 base = b - dir * head;
col = drawLine(col, p, a, base + dir * pixelSize(), widthPx, ink);
vec2 q = p - b;
float x = dot(q, -dir), y = abs(dot(q, side));
float inside = step(0.0, x) * step(x, head) * step(y, x * 0.5);
float edge = max(-x, max(x - head, y - x * 0.5)) / pixelSize();
return mix(col, ink, max(inside, saturate(0.5 - edge)));
}
vec3 drawCircle(vec3 col, vec2 p, vec2 c, float r, float widthPx, vec3 ink) {
float d = abs(length(p - c) - r) / pixelSize();
return mix(col, ink, saturate(0.5 * widthPx + 0.5 - d));
}
// A dot with a ring in the surface color around it.
vec3 drawDot(vec3 col, vec2 p, vec2 c, float radiusPx, vec3 ink) {
float d = length(p - c) / pixelSize();
col = mix(col, themeSurface(), saturate(radiusPx + 2.5 - d));
return mix(col, ink, saturate(radiusPx + 0.5 - d));
}
// Fill or outline any signed distance d (in centered() units).
vec3 fillShape(vec3 col, float d, vec3 ink, float alpha) {
return mix(col, ink, alpha * saturate(0.5 - d / pixelSize()));
}
vec3 strokeShape(vec3 col, float d, float widthPx, vec3 ink) {
return mix(col, ink, saturate(0.5 * widthPx + 0.5 - abs(d) / pixelSize()));
}
// Distance field view: pink outside, mint inside, a band every `spacing` units, the zero line in ink.
vec3 drawDistanceField(float d, float spacing) {
vec3 hue = d > 0.0 ? series2() : series1();
float strength = mix(0.2, 0.7, 1.0 - exp(-5.0 * abs(d)));
strength *= 0.75 + 0.25 * cos(TAU * d / spacing);
vec3 col = mix(themeSurface(), hue, strength);
return strokeShape(col, d, 2.0, themeInk());
}
#endifray
A camera ray per pixel and exact hits with spheres and planes, for small 3D scenes.
Show ray.glsl
// Small 3D toolkit: a camera ray per pixel and analytic hits, for demos that need a real sphere or plane.
#ifndef DOCME_RAY
#define DOCME_RAY
// Ray from a camera at `eye` looking at `target`. uv in centered() units; zoom ~1.5 is a normal lens.
vec3 cameraRay(vec2 uv, vec3 eye, vec3 target, float zoom) {
vec3 f = normalize(target - eye);
vec3 r = normalize(cross(vec3(0.0, 1.0, 0.0), f));
vec3 u = cross(f, r);
return normalize(uv.x * r + uv.y * u + zoom * f);
}
// Distance along the ray to a sphere, or -1 when it misses.
float raySphere(vec3 ro, vec3 rd, vec3 center, float radius) {
vec3 oc = ro - center;
float b = dot(oc, rd);
float c = dot(oc, oc) - radius * radius;
float h = b * b - c;
if (h < 0.0) return -1.0;
return -b - sqrt(h);
}
// Distance along the ray to the plane dot(p, n) = d, or -1 when it is behind the ray.
float rayPlane(vec3 ro, vec3 rd, vec3 n, float d) {
float denom = dot(rd, n);
if (abs(denom) < 1e-5) return -1.0;
float t = (d - dot(ro, n)) / denom;
return t > 0.0 ? t : -1.0;
}
// Dragging a 3D demo (pointer="orbit"): sideways spins the object without end, up and down tilts it up to 75 degrees.
vec2 orbitAngles(vec2 pointer01) {
return vec2((pointer01.x - 0.5) * PI, clamp((pointer01.y - 0.5) * PI, -1.3, 1.3));
}
// The object's own coordinates of a point p seen in view space, after the reader turned the object.
// The front of the object follows the drag: right moves it right, up tilts it up.
vec3 orbitTurn(vec3 p, vec2 pointer01) {
vec2 a = orbitAngles(pointer01);
float cp = cos(a.y), sp = sin(a.y);
p = vec3(p.x, cp * p.y + sp * p.z, cp * p.z - sp * p.y);
float cy = cos(a.x), sy = sin(a.x);
return vec3(cy * p.x + sy * p.z, p.y, cy * p.z - sy * p.x);
}
// Orbit camera position from the same drag, for scenes where the camera moves instead of the object.
vec3 orbitEye(vec2 pointer01, float dist) {
vec2 a = orbitAngles(pointer01);
return dist * vec3(-sin(a.x) * cos(a.y), -sin(a.y), -cos(a.x) * cos(a.y));
}
#endifaudio
A made-up music signal that stands in for AudioLink.
Show audio.glsl
// A made-up music signal standing in for AudioLink in the browser.
// AudioLink (by llealloo) gives VRChat shaders four bands: bass, low mids, high mids, treble,
// each 0..1, plus a history of past values. These functions fake the same shape from time alone.
#ifndef DOCME_AUDIO
#define DOCME_AUDIO
#define AUDIO_BPM 120.0
// Sharp attack, quick decay, once per beat.
float beatPulse(float t, float beatsPerHit, float decay) {
float phase = fract(t * AUDIO_BPM / 60.0 / beatsPerHit);
return exp(-decay * phase);
}
// The four bands at time t, each 0..1.
vec4 audioBands(float t) {
float bass = beatPulse(t, 1.0, 6.0);
float lowMid = 0.55 * beatPulse(t + 0.25, 2.0, 4.0) + 0.25 * (0.5 + 0.5 * sin(t * 1.3));
float highMid = 0.5 * beatPulse(t + 0.125, 0.5, 9.0) * (0.6 + 0.4 * sin(t * 0.7));
float treble = 0.35 * beatPulse(t + 0.375, 0.25, 14.0) * hash11(floor(t * 8.0) + 3.0);
return saturate(vec4(bass, lowMid, highMid, treble));
}
// AudioLink keeps a short history per band; reading further back means reading an older value.
vec4 audioHistory(float t, float secondsAgo) {
return audioBands(t - secondsAgo);
}
#endif