shadertown 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +2 -2
- package/cli/mcp.mjs +1 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +31 -8
- package/dist/export.js.map +1 -1
- package/dist/generated/sources.d.ts +89 -5
- package/dist/generated/sources.d.ts.map +1 -1
- package/dist/generated/sources.js +88 -5
- package/dist/generated/sources.js.map +1 -1
- package/dist/graph.d.ts +17 -12
- package/dist/graph.d.ts.map +1 -1
- package/dist/graph.js +424 -210
- package/dist/graph.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -0
- package/dist/react.js.map +1 -1
- package/dist/registry.d.ts +212 -70
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +127 -45
- package/dist/registry.js.map +1 -1
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +8 -3
- package/dist/renderer.js.map +1 -1
- package/dist/source.d.ts +29 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +102 -9
- package/dist/source.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +10 -9
|
@@ -5,18 +5,19 @@ export declare const shaderSources: {
|
|
|
5
5
|
readonly beacon: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// beacon\n// ---------------------------------------------------------------------------\n\n// Beacon — a triangle of LEDs standing on a floor that catches what they throw.\n//\n// The glow is computed, never blurred. Each edge contributes an exponential\n// falloff off the line itself and a second one off the emitter nearest along\n// it, both analytic, so the halo is the same shape at any zoom and the LEDs\n// stay round at any pixel density. The floor is the same light field evaluated\n// at the mirrored point, which is a reflection for the price of a second\n// lookup.\n\nstruct Edge {\n strip : f32,\n cells : f32,\n}\n\nfn beaCross(a : vec2f, b : vec2f) -> f32 {\n return a.x * b.y - a.y * b.x;\n}\n\n// One edge, seen from `p`: the continuous strip, then its discrete emitters.\nfn beaEdge(p : vec2f, a : vec2f, b : vec2f, count : f32, bleed : f32, chase : f32, t : f32, seed : f32) -> Edge {\n let ba = b - a;\n let pa = p - a;\n let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n\n let strip = exp(-length(pa - ba * h) / max(bleed, 0.003));\n\n // Three cells, not one: the projection only ever names the nearest, and\n // switching between them leaves a seam wherever the halo is still bright.\n let index = floor(h * count);\n var cells = 0.0;\n for (var k = -1; k <= 1; k = k + 1) {\n let idx = clamp(index + f32(k), 0.0, count - 1.0);\n let centre = a + ba * ((idx + 0.5) / count);\n let pulse = mix(1.0, 0.5 + 0.5 * sin((idx / count) * TAU * 2.0 - t * 3.2 + seed), chase);\n cells = cells + exp(-length(p - centre) / max(bleed * 0.30, 0.001)) * pulse;\n }\n\n return Edge(strip, cells);\n}\n\n// Everything the three edges and the pointer put into one point.\nfn beaField(p : vec2f, v0 : vec2f, v1 : vec2f, v2 : vec2f, bleed : f32, t : f32) -> vec3f {\n let count = max(floor(params.a), 2.0);\n let chase = clamp(params.d, 0.0, 1.0);\n\n let e0 = beaEdge(p, v0, v1, count, bleed, chase, t, 0.0);\n let e1 = beaEdge(p, v1, v2, count, bleed, chase, t, 2.094);\n let e2 = beaEdge(p, v2, v0, count, bleed, chase, t, 4.188);\n\n let c0 = params.colorB.rgb;\n let c1 = params.colorC.rgb;\n let c2 = mix(params.colorB.rgb, params.colorC.rgb, 0.5);\n\n var light = c0 * (e0.strip * 0.35 + e0.cells);\n light = light + c1 * (e1.strip * 0.35 + e1.cells);\n light = light + c2 * (e2.strip * 0.35 + e2.cells);\n\n // A roving lamp under the cursor, so the frame answers the pointer even\n // though nothing in it moves.\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), 0.5 - params.pointer.y);\n light = light + c1 * exp(-length(p - cursor) / max(bleed * 2.2, 0.01)) * 0.7;\n\n return light;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n // Flipped once, here: uv runs down the screen, and everything below reads\n // better written the way it is meant to be seen — apex up, floor beneath.\n let p = st_centered(uv) * vec2f(1.0, -1.0);\n\n let size = 0.30 * clamp(params.scale, 0.2, 3.0);\n // A rock rather than a spin: this is a front, and a front that turns all the\n // way round stops reading as one.\n let tilt = sin(t * 0.35) * 0.22;\n let lift = 0.06;\n let v0 = st_rot(tilt) * (vec2f(0.0, 1.0) * size) + vec2f(0.0, lift);\n let v1 = st_rot(tilt) * (vec2f(-0.866, -0.5) * size) + vec2f(0.0, lift);\n let v2 = st_rot(tilt) * (vec2f(0.866, -0.5) * size) + vec2f(0.0, lift);\n\n let bleed = max(params.b, 0.004);\n let floorY = -0.30;\n\n var col = params.colorA.rgb * 0.10;\n col = col + beaField(p, v0, v1, v2, bleed, t);\n\n // The plate the LEDs are mounted on. Signed distance to a convex polygon is\n // exact inside it, which is all a fill mask needs.\n let inside = min(\n min(beaCross(normalize(v1 - v0), p - v0), beaCross(normalize(v2 - v1), p - v1)),\n beaCross(normalize(v0 - v2), p - v2),\n );\n // One pixel wide at any density. A fixed width is a hairline on a phone and\n // a smear on a 5K panel, and this is the only hard edge in the frame.\n let plateEdge = max(fwidth(inside), 0.00001);\n let plate = smoothstep(-plateEdge, plateEdge, inside);\n col = mix(col, params.colorA.rgb * 0.30 + col * 0.25, plate);\n\n // Floor: the same field read at the mirrored point, widened with depth so it\n // smears the way a brushed surface does, and faded as it goes.\n let floorEdge = max(fwidth(p.y), 0.00001);\n let below = smoothstep(floorY + floorEdge, floorY - floorEdge, p.y);\n let depth = max(floorY - p.y, 0.0);\n let mirrored = vec2f(p.x, 2.0 * floorY - p.y);\n let rough = bleed * (1.0 + depth * 7.0);\n let bounce = beaField(mirrored, v0, v1, v2, rough, t) * exp(-depth * 4.5) * clamp(params.c, 0.0, 1.0);\n col = mix(col, params.colorA.rgb * 0.05 + bounce, below);\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
6
6
|
readonly bloom: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// bloom\n// ---------------------------------------------------------------------------\n\n// Bloom — light bleeding out of the bright parts of the layer below.\n\n// Only what is already bright blooms. The soft knee keeps the threshold from\n// showing up as a hard outline around every highlight.\nfn bloom_bright(uv : vec2f) -> vec3f {\n let c = st_srcRgb(uv);\n return c * smoothstep(params.a, params.a + 0.25, st_luma(c));\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let base = st_srcRgb(uv);\n\n // A wide spiral over the thresholded image. Thresholding per tap, rather\n // than blurring first, is what keeps a dark neighbour from dimming a bloom.\n let taps = 16;\n let radius = params.b * 0.12;\n // Stretching the spiral sideways gives the horizontal streak an anamorphic\n // lens leaves on every point of light.\n let stretch = vec2f(1.0 + params.warp * 3.0, 1.0);\n\n var glow = vec3f(0.0);\n for (var i = 0; i < taps; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / f32(taps));\n glow = glow + bloom_bright(uv + vec2f(cos(angle), sin(angle)) * r * stretch);\n }\n glow = glow / f32(taps);\n\n // Tint pulls the halo towards the palette — the coating on a real lens is\n // rarely neutral, and a warm bloom over cool light is most of the look.\n glow = mix(glow, glow * st_palette(st_luma(glow)) * 2.0, clamp(params.d, 0.0, 1.0));\n\n let col = base + glow * params.c;\n return st_post(col * params.intensity, uv);\n}\n";
|
|
7
7
|
readonly blossom: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// blossom\n// ---------------------------------------------------------------------------\n\n// Blossom — one flower, drawn large enough to be the header.\n//\n// Two rings of rose-curve petals, the inner rotated half a sector and\n// darkened, around a phyllotaxis seed head: florets at r = c*sqrt(n),\n// theta = n * 137.508 degrees, found by inverting the radius and testing a\n// handful of candidate indices. Each seed gets a domed normal, so the head\n// shades like geometry without a single ray marched.\n\nconst GOLDEN_ANGLE : f32 = 2.39996323;\n\nfn petalRing(p : vec2f, count : f32, radius : f32, spin : f32, aa : f32) -> f32 {\n let r = length(p);\n let ang = atan2(p.y, p.x) + spin;\n // The rose curve, sharpened so petals separate instead of scalloping.\n let lobe = pow(abs(cos(count * ang * 0.5)), 1.4);\n let edge = radius * (0.35 + 0.65 * lobe);\n return smoothstep(edge + aa, edge - aa, r);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale * 2.2;\n\n // The whole flower breathes and leans, more at the rim than the heart.\n let breathe = 1.0 + 0.03 * sin(t * 0.7) * params.b;\n p = st_rot(0.05 * sin(t * 0.45) + t * 0.02) * p * (1.0 / breathe);\n\n let aa = max(fwidth(p.x), 0.003);\n let r = length(p);\n\n // Backdrop: a quiet radial pool of colorA, so the flower owns the light.\n var col = params.colorA.rgb * mix(1.1, 0.45, smoothstep(0.0, 1.4, r));\n\n // Pollen: a dozen soft motes drifting up behind the flower, each a small\n // gaussian rather than a lit pixel.\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let rnd = st_hash22(vec2f(fi * 3.7, fi * 1.9));\n let cycle = fract(t * (0.02 + rnd.y * 0.03) + rnd.x);\n let motePos = vec2f(\n (rnd.x - 0.5) * 2.6 + sin(t * 0.3 + fi) * 0.08,\n (0.5 - cycle) * 2.2,\n );\n let fade = smoothstep(0.0, 0.2, cycle) * smoothstep(1.0, 0.8, cycle);\n col = col + params.colorC.rgb * exp(-length(p - motePos) * mix(24.0, 60.0, rnd.y)) * fade * 0.14 * params.c;\n }\n\n let petals = floor(params.a);\n let ringR = 0.78;\n\n // Outer ring: petals lit from the heart outward, edged a shade darker so\n // each one separates from its neighbours.\n let outer = petalRing(p, petals, ringR, 0.0, aa);\n if (outer > 0.0) {\n let ang = atan2(p.y, p.x);\n let lobe = pow(abs(cos(petals * ang * 0.5)), 1.4);\n let edge = ringR * (0.35 + 0.65 * lobe);\n var tone = mix(params.colorB.rgb, params.colorC.rgb, smoothstep(0.1, edge, r));\n tone = tone * (0.92 + 0.08 * lobe);\n // The last stretch before the rim falls into shadow — a drawn outline\n // without drawing one.\n tone = tone * mix(1.0, 0.72, smoothstep(edge - 0.09, edge, r));\n col = mix(col, tone, outer);\n }\n\n // Inner ring: half a sector round, smaller, in the outer ring's shadow.\n let inner = petalRing(p, petals, ringR * 0.58, PI / max(petals, 1.0), aa);\n if (inner > 0.0) {\n let tone = mix(params.colorB.rgb * 0.55, params.colorC.rgb * 0.7, smoothstep(0.05, ringR * 0.5, r));\n col = mix(col, tone, inner);\n }\n\n // The seed head. Invert r = c*sqrt(n) for a first guess at the floret,\n // then test neighbours — the true nearest can be a Fibonacci stride away,\n // but at this size a miss reads as texture.\n let headR = 0.26 * params.d;\n if (r < headR) {\n let c = headR / sqrt(320.0);\n let guess = (r / c) * (r / c);\n var best = 1e9;\n var bestSeed = vec2f(0.0);\n var bestN = 0.0;\n for (var k = -3; k <= 3; k = k + 1) {\n let n = max(round(guess) + f32(k), 0.0);\n let q = c * sqrt(n) * vec2f(cos(n * GOLDEN_ANGLE + t * 0.05), sin(n * GOLDEN_ANGLE + t * 0.05));\n let d = length(p - q);\n if (d < best) {\n best = d;\n bestSeed = q;\n bestN = n;\n }\n }\n let seedR = c * 0.75;\n let dome = clamp(1.0 - (best / seedR) * (best / seedR), 0.0, 1.0);\n // A domed normal lit from the upper left, no geometry required.\n let nrm = normalize(vec3f((p - bestSeed) / seedR, sqrt(max(dome, 0.02))));\n let lit = clamp(dot(nrm, normalize(vec3f(-0.5, 0.6, 0.65))), 0.0, 1.0);\n var head = params.colorC.rgb * (0.25 + 0.75 * lit) * (0.7 + 0.3 * st_hash11(bestN));\n head = mix(params.colorB.rgb * 0.2, head, smoothstep(0.0, 0.25, dome));\n col = mix(col, head, smoothstep(headR, headR - aa * 2.0, r));\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
8
|
+
readonly blueprint: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the one function the flavour asks for. Two flavours so far:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// @reads-source\n// shadertown drafting prelude — prepended (after the shared, source and graph\n// preludes) to every shader in src/shaders/graph/drafting/.\n//\n// A drafting graph reads a layer the way a draughtsman reads a part: it\n// measures it first, then draws around it. The passes, in the order the\n// runtime (src/graph.ts) encodes them:\n//\n// fs_bounds the layer's ink, boxed — a 256² sampling of the source\n// reduced to a 1×1 texture holding (min, max) in source uv\n// fs_reduce one 4×4 reduction step of that box, run three times\n// fs_seed the box, padded, rasterised into a 512² grid: edge texels\n// become seeds for the jump flood\n// fs_jfa one jump-flood step; ten of them turn seeds into the\n// nearest edge for every texel\n// fs_distance nearest edge to a signed distance in source pixels, and\n// a filterable texture the plate pass can sample\n// fs_plate the drawing: supplied by the variant file\n// fs_brightPlate the bloom gate — the plate's alpha, not its luminance, so\n// a bright mat does not glow and the lines on it do\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_plate(uv : vec2f) -> vec4f\n//\n// `uv` is the canvas uv; the return value is linear-light colour plus, in w,\n// how much of this pixel is ink the bloom chain may pick up. The helpers here\n// hand it the measured box, the fitted drawing rectangle, and the distance\n// field, so a variant is the look and none of the plumbing.\n\nstruct DraftUniforms {\n // jump-flood step in texels, distance-field grid size, two spares\n step : vec4f,\n}\n\n@group(0) @binding(4) var<uniform> du : DraftUniforms;\n// (min.xy, max.xy) of the ink, in source uv. 1×1 after the reductions.\n@group(0) @binding(5) var t_bounds : texture_2d<f32>;\n// The previous reduction level, or a jump-flood ping-pong partner.\n@group(0) @binding(6) var t_reduce : texture_2d<f32>;\n@group(0) @binding(7) var t_jfa : texture_2d<f32>;\n// The finished signed distance field: x is distance in source pixels, y the\n// soft ink at that texel.\n@group(0) @binding(8) var t_sdf : texture_2d<f32>;\n// The label face: sixteen glyphs in a strip, drawn by the runtime with the\n// browser's monospace font. Alpha is coverage.\n@group(0) @binding(9) var t_glyphs : texture_2d<f32>;\n\n/** The sampling grid the bounds pass reads the source through, per side. */\nconst ST_BOUNDS_GRID : f32 = 256.0;\n\n// --- ink ---------------------------------------------------------------------\n\n/**\n * How much ink sits at a source uv, 0 to 1.\n *\n * Bright is ink: the convention the text and SVG sources render to. The\n * threshold is the shader's `a` slot so a photograph can be cut where it\n * reads best, and the band around it is wide enough to keep the source's own\n * anti-aliasing rather than reintroduce stair steps. Outside the source there\n * is no ink at all, so a layer with ink on its border still gets an outline\n * there instead of the sampler's smear.\n */\nfn st_ink(srcUv : vec2f) -> f32 {\n let luma = st_luma(st_srcAt(srcUv).rgb);\n let threshold = clamp(params.a, 0.05, 0.95);\n return smoothstep(threshold - 0.12, threshold + 0.12, luma) * st_srcInside(srcUv);\n}\n\n// --- the box ----------------------------------------------------------------\n\n/** The sentinel an empty box carries: min above max, so `max(min)` heals it. */\nconst ST_EMPTY_BOX : vec4f = vec4f(2.0, 2.0, -1.0, -1.0);\n\n@fragment fn fs_bounds(@builtin(position) position : vec4f) -> @location(0) vec4f {\n // Each texel of the 64² output owns a 4×4 block of the 256² sampling grid.\n let block = floor(position.xy) * 4.0;\n var box = ST_EMPTY_BOX;\n for (var y = 0; y < 4; y = y + 1) {\n for (var x = 0; x < 4; x = x + 1) {\n let uv = (block + vec2f(f32(x), f32(y)) + 0.5) / ST_BOUNDS_GRID;\n if (st_ink(uv) > 0.5) {\n box = vec4f(min(box.xy, uv), max(box.zw, uv));\n }\n }\n }\n return box;\n}\n\n@fragment fn fs_reduce(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let block = vec2i(position.xy) * 4;\n var box = ST_EMPTY_BOX;\n for (var y = 0; y < 4; y = y + 1) {\n for (var x = 0; x < 4; x = x + 1) {\n let v = textureLoad(t_reduce, block + vec2i(x, y), 0);\n box = vec4f(min(box.xy, v.xy), max(box.zw, v.zw));\n }\n }\n return box;\n}\n\n/**\n * The measured box, in source uv, grown by half a sampling cell so the ink\n * the grid found is inside it rather than on its edge. A layer with no ink at\n * all boxes the whole layer: there is still a plate to draw.\n */\nfn st_plateBox() -> vec4f {\n let b = textureLoad(t_bounds, vec2i(0), 0);\n if (b.z < b.x || b.w < b.y) {\n return vec4f(0.0, 0.0, 1.0, 1.0);\n }\n let halfCell = 0.5 / ST_BOUNDS_GRID;\n return vec4f(b.xy - halfCell, b.zw + halfCell);\n}\n\n/** The box in source pixels: what the dimension lines measure. */\nfn st_plateBoxPx(box : vec4f) -> vec2f {\n return max((box.zw - box.xy) * max(source.size, vec2f(1.0)), vec2f(1.0));\n}\n\n/**\n * The region the distance field covers: the box plus a margin on every side,\n * so the outline and anything drawn just off the ink has a distance to read.\n * A tenth of the longer side, which is more than the widest stroke a control\n * can ask for.\n */\nfn st_plateField(box : vec4f) -> vec4f {\n let sizePx = st_plateBoxPx(box);\n let padPx = max(sizePx.x, sizePx.y) * 0.1 + 2.0;\n let pad = padPx / max(source.size, vec2f(1.0));\n return vec4f(box.xy - pad, box.zw + pad);\n}\n\n/** Source pixels per distance-field texel, per axis. */\nfn st_fieldTexelPx(field : vec4f, grid : f32) -> vec2f {\n return (field.zw - field.xy) * max(source.size, vec2f(1.0)) / grid;\n}\n\n// --- the distance field ---------------------------------------------------------\n\n/**\n * Seeds: the texels where the ink changes between neighbours. Coordinates are\n * texel indices, and a texel that is not a seed carries −1 so the flood can\n * tell. z keeps the soft ink for the sign later, w says whether this texel\n * itself was an edge.\n */\n@fragment fn fs_seed(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let grid = du.step.y;\n let field = st_plateField(st_plateBox());\n let texel = vec2f(1.0) / grid;\n let coord = floor(position.xy);\n let uv = (coord + 0.5) * texel;\n let at = field.xy + uv * (field.zw - field.xy);\n let span = (field.zw - field.xy) * texel;\n\n let here = st_ink(at);\n let side = step(0.5, here);\n var edge = 0.0;\n edge = max(edge, abs(step(0.5, st_ink(at + vec2f(span.x, 0.0))) - side));\n edge = max(edge, abs(step(0.5, st_ink(at - vec2f(span.x, 0.0))) - side));\n edge = max(edge, abs(step(0.5, st_ink(at + vec2f(0.0, span.y))) - side));\n edge = max(edge, abs(step(0.5, st_ink(at - vec2f(0.0, span.y))) - side));\n\n let seed = select(vec2f(-1.0), coord, edge > 0.5);\n return vec4f(seed, here, edge);\n}\n\n/**\n * One jump-flood step: look `step` texels away in the eight directions, keep\n * whichever seed is nearest. Distances are measured in source pixels, not\n * texels, because the box is rarely square and the field grid always is.\n */\n@fragment fn fs_jfa(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let n = i32(textureDimensions(t_jfa).x);\n let grid = f32(n);\n let texelPx = st_fieldTexelPx(st_plateField(st_plateBox()), grid);\n let coord = vec2i(position.xy);\n let here = vec2f(coord) + 0.5;\n let jump = i32(du.step.x);\n\n let own = textureLoad(t_jfa, coord, 0);\n var best = own.xy;\n var bestD = select(1e20, length((own.xy + 0.5 - here) * texelPx), own.x >= 0.0);\n\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let q = coord + vec2i(x, y) * jump;\n if (q.x < 0 || q.y < 0 || q.x >= n || q.y >= n) { continue; }\n let s = textureLoad(t_jfa, q, 0);\n if (s.x < 0.0) { continue; }\n let d = length((s.xy + 0.5 - here) * texelPx);\n if (d < bestD) {\n bestD = d;\n best = s.xy;\n }\n }\n }\n return vec4f(best, own.z, own.w);\n}\n\n/**\n * Nearest edge to a signed distance, in source pixels: negative inside the\n * ink. An edge texel estimates its own sub-texel offset from the soft ink,\n * which is what lets a bilinear read of this texture land the outline on the\n * source's own anti-aliased boundary rather than on the texel grid.\n */\n@fragment fn fs_distance(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let grid = f32(textureDimensions(t_jfa).x);\n let texelPx = st_fieldTexelPx(st_plateField(st_plateBox()), grid);\n let coord = vec2i(position.xy);\n let v = textureLoad(t_jfa, coord, 0);\n let here = vec2f(coord) + 0.5;\n\n let far = length(texelPx) * grid;\n let d = select(far, length((v.xy + 0.5 - here) * texelPx), v.x >= 0.0);\n let withSign = select(d, -d, v.z > 0.5);\n let atEdge = (0.5 - v.z) * 2.0 * min(texelPx.x, texelPx.y);\n return vec4f(select(withSign, atEdge, v.w > 0.5), v.z, 0.0, 1.0);\n}\n\n// --- the drawing ----------------------------------------------------------------\n\n/**\n * Where the measured box lands on the canvas.\n *\n * Contained, never covered: the whole part has to be on the plate for the\n * dimension lines to mean anything. `zoom` is the fraction of the canvas the\n * part may fill on its limiting axis; the rectangle is in the centred,\n * aspect-corrected space `st_centered` gives, y down, one unit tall.\n */\nstruct PlateFit {\n // top-left and bottom-right, centred canvas units\n min : vec2f,\n max : vec2f,\n // canvas device pixels per source pixel\n scale : f32,\n}\n\nfn st_plateFit(box : vec4f, zoom : f32, centre : vec2f) -> PlateFit {\n let sizePx = st_plateBoxPx(box);\n let avail = vec2f(st_aspect(), 1.0) * zoom;\n let unitsPerPx = min(avail.x / sizePx.x, avail.y / sizePx.y);\n let size = sizePx * unitsPerPx;\n var fit : PlateFit;\n fit.min = centre - size * 0.5;\n fit.max = centre + size * 0.5;\n fit.scale = unitsPerPx * params.resolution.y;\n return fit;\n}\n\n/** A canvas position, in `st_centered` units, to the source uv under it. */\nfn st_plateToSource(p : vec2f, box : vec4f, fit : PlateFit) -> vec2f {\n return box.xy + (p - fit.min) / (fit.max - fit.min) * (box.zw - box.xy);\n}\n\n/**\n * Signed distance to the ink under a canvas position, in canvas device\n * pixels. Off the field it is simply far away.\n */\nfn st_plateDistance(p : vec2f, box : vec4f, fit : PlateFit) -> f32 {\n let field = st_plateField(box);\n let srcUv = st_plateToSource(p, box, fit);\n let fuv = (srcUv - field.xy) / (field.zw - field.xy);\n let inside = step(0.0, fuv.x) * step(fuv.x, 1.0) * step(0.0, fuv.y) * step(fuv.y, 1.0);\n let d = textureSample(t_sdf, linearSampler, clamp(fuv, vec2f(0.0), vec2f(1.0))).x * fit.scale;\n return select(1e5, d, inside > 0.5);\n}\n\n// --- lettering --------------------------------------------------------------------\n// The dimension labels are set from a glyph atlas the runtime draws once with\n// the browser's monospace face (src/graph.ts, `GLYPHS`): sixteen cells in a\n// strip, in this order — the digits, a colon, W, H, p, x and a space. A label\n// is laid out in cells the height of one glyph and `ST_GLYPH_ADVANCE` of that\n// wide, and each cell samples its glyph out of the strip.\n\nconst ST_GLYPH_COUNT : f32 = 16.0;\n/** Cell width over cell height in the atlas — 40 over 64. */\nconst ST_GLYPH_ADVANCE : f32 = 0.625;\n\nconst ST_GLYPH_COLON : u32 = 10u;\nconst ST_GLYPH_W : u32 = 11u;\nconst ST_GLYPH_H : u32 = 12u;\nconst ST_GLYPH_P : u32 = 13u;\nconst ST_GLYPH_X : u32 = 14u;\nconst ST_GLYPH_SPACE : u32 = 15u;\n\n/**\n * Coverage of one glyph at a position inside its cell, 0 to 1 across and\n * down. Sampled at level zero by choice: the call sits inside a branch, and\n * the mip-free form has no uniformity to keep.\n */\nfn st_glyph(glyph : u32, cell : vec2f) -> f32 {\n let texel = 1.0 / vec2f(textureDimensions(t_glyphs));\n let inCell = clamp(cell, texel * vec2f(ST_GLYPH_COUNT, 1.0), vec2f(1.0) - texel * vec2f(ST_GLYPH_COUNT, 1.0));\n let uv = vec2f((f32(glyph) + inCell.x) / ST_GLYPH_COUNT, inCell.y);\n return textureSampleLevel(t_glyphs, linearSampler, uv, 0.0).a;\n}\n\n/** How many digits a count prints with, at least one. */\nfn st_digitCount(value : u32) -> u32 {\n var n = 1u;\n var v = value;\n while (v >= 10u) {\n v = v / 10u;\n n = n + 1u;\n }\n return n;\n}\n\n/** The glyph at position `slot` of \"W: 479 px\" — `axis` picks the letter. */\nfn st_labelGlyph(slot : u32, axis : u32, value : u32) -> u32 {\n let digits = st_digitCount(value);\n if (slot == 0u) { return axis; }\n if (slot == 1u) { return ST_GLYPH_COLON; }\n if (slot == 2u) { return ST_GLYPH_SPACE; }\n if (slot < 3u + digits) {\n // Digits print most significant first: peel from the right.\n var v = value;\n for (var i = 3u + digits - 1u; i > slot; i = i - 1u) { v = v / 10u; }\n return v % 10u;\n }\n if (slot == 3u + digits) { return ST_GLYPH_SPACE; }\n if (slot == 4u + digits) { return ST_GLYPH_P; }\n if (slot == 5u + digits) { return ST_GLYPH_X; }\n return ST_GLYPH_SPACE;\n}\n\n/** Width of the label, in glyph heights. */\nfn st_labelWidth(value : u32) -> f32 {\n return f32(6u + st_digitCount(value)) * ST_GLYPH_ADVANCE;\n}\n\n/**\n * Coverage of the label at a position measured in glyph heights, origin at\n * the label's top-left. `st_labelWidth` says how far it runs; it is one\n * glyph tall.\n */\nfn st_label(tp : vec2f, axis : u32, value : u32) -> f32 {\n if (tp.x < 0.0 || tp.y < 0.0 || tp.y >= 1.0 || tp.x >= st_labelWidth(value)) { return 0.0; }\n let slot = floor(tp.x / ST_GLYPH_ADVANCE);\n let cell = vec2f((tp.x - slot * ST_GLYPH_ADVANCE) / ST_GLYPH_ADVANCE, tp.y);\n return st_glyph(st_labelGlyph(u32(slot), axis, value), cell);\n}\n\n// --- the passes the runtime drives ---------------------------------------------\n\n@fragment fn fs_plate(@location(0) uv : vec2f) -> @location(0) vec4f {\n return st_plate(uv);\n}\n\n/**\n * The bloom gate for a plate: what the variant marked as ink, not what is\n * bright. A vellum-coloured mat would otherwise bloom as a whole.\n */\n@fragment fn fs_brightPlate(@location(0) uv : vec2f) -> @location(0) vec4f {\n let texel = textureSample(t_scene, linearSampler, uv);\n return vec4f(texel.rgb * texel.a, texel.a);\n}\n\n// ---------------------------------------------------------------------------\n// blueprint\n// ---------------------------------------------------------------------------\n\n// Blueprint — the layer measured and drawn up as a technical plate.\n//\n// The ink of the layer (a logo, a wordmark, a line of text) is boxed, its\n// outline is inked at a set weight, and the inside is hatched at forty-five\n// degrees. Below and beside it run the dimension lines a draughtsman would\n// add — ticks, extension lines and a label with the size in pixels, set in a\n// bitmap face that lives in the shader. Behind it all is the mat: grained\n// paper under a fine grid, with construction lines meeting at the pointer.\n//\n// The plumbing — the box, the fit, the distance field — is the drafting\n// prelude's. This file is the look: three wells, four slots.\n// colorA the mat, colorB the ink, colorC the hatching and the grid\n// a threshold, b hatch spacing, c line weight, d grid cells\n\n/** sRGB well to linear light, so the present pass's sRGB step undoes it. */\nfn bp_linear(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return pow(x, vec3f(2.2));\n}\n\n/** A one-device-pixel line at distance `d` px, one pixel of anti-aliasing. */\nfn bp_hairline(d : f32, weight : f32) -> f32 {\n return 1.0 - smoothstep(weight * 0.5 - 0.6, weight * 0.5 + 0.6, d);\n}\n\n/** Distance from `p` to the segment a–b, all in device pixels. */\nfn bp_segment(p : vec2f, a : vec2f, b : vec2f) -> f32 {\n let ab = b - a;\n let t = clamp(dot(p - a, ab) / max(dot(ab, ab), 1e-6), 0.0, 1.0);\n return length(p - (a + ab * t));\n}\n\nfn st_plate(uv : vec2f) -> vec4f {\n let res = params.resolution;\n let t = st_time();\n let p = st_centered(uv);\n // Everything below is measured in device pixels: one canvas unit is the\n // canvas's height, so lines keep their weight whatever the resolution.\n let px = p * res.y;\n\n let paper = bp_linear(params.colorA.rgb);\n let ink = bp_linear(params.colorB.rgb);\n let pencil = bp_linear(params.colorC.rgb);\n\n let weight = clamp(params.c, 0.5, 8.0);\n let hair = max(1.0, weight * 0.4);\n\n // --- the part, fitted to the plate -------------------------------------------\n // Sits a little high, so the width label below it has its own room.\n let box = st_plateBox();\n let zoom = clamp(params.scale, 0.3, 1.6) * 0.72;\n let fit = st_plateFit(box, zoom, vec2f(0.0, -0.03));\n let fitMin = fit.min * res.y;\n let fitMax = fit.max * res.y;\n let sd = st_plateDistance(p, box, fit);\n\n // --- the mat -------------------------------------------------------------------\n // Paper, not film: the speckle stays put while everything else moves.\n let speckle = st_hash21(floor(uv * res));\n let fibre = st_noise2(uv * res * 0.11);\n var col = paper * (0.90 + speckle * 0.16 + (fibre - 0.5) * 0.10);\n\n // The grid: `d` is how many cells run down the height, drawn one pixel\n // wide, with every fourth line heavier the way graph paper is ruled. Zero\n // cells is no grid, and no construction lines either.\n let cells = clamp(params.d, 0.0, 32.0);\n let grid = step(0.5, cells);\n let cell = res.y / max(cells, 1.0);\n let gridUv = px + res * 0.5;\n let minorD = abs(fract(gridUv / cell + 0.5) - 0.5) * cell;\n let majorD = abs(fract(gridUv / (cell * 4.0) + 0.5) - 0.5) * cell * 4.0;\n let minorLine = bp_hairline(min(minorD.x, minorD.y), 1.0);\n let majorLine = bp_hairline(min(majorD.x, majorD.y), 1.0);\n col = mix(col, pencil, max(minorLine * 0.18, majorLine * 0.45) * grid);\n\n // Construction lines from the corners of the sheet meet at the pointer, and\n // a crosshair marks the meeting. With no hand on the canvas the pointer is\n // the centre, which is where a draughtsman would put it anyway.\n let cursor = (params.pointer - 0.5) * res;\n let sheet = res * 0.5;\n var diag = 1e5;\n diag = min(diag, bp_segment(px, cursor, vec2f(-sheet.x, -sheet.y)));\n diag = min(diag, bp_segment(px, cursor, vec2f( sheet.x, -sheet.y)));\n diag = min(diag, bp_segment(px, cursor, vec2f(-sheet.x, sheet.y)));\n diag = min(diag, bp_segment(px, cursor, vec2f( sheet.x, sheet.y)));\n col = mix(col, pencil, bp_hairline(diag, 1.0) * 0.22 * grid);\n\n let arm = res.y * 0.014;\n let toCursor = abs(px - cursor);\n let hairCross = max(\n bp_hairline(toCursor.y, 1.0) * step(toCursor.x, arm) * step(arm * 0.25, toCursor.x),\n bp_hairline(toCursor.x, 1.0) * step(toCursor.y, arm) * step(arm * 0.25, toCursor.y),\n );\n col = mix(col, ink, hairCross * 0.8);\n\n // --- the part -------------------------------------------------------------------\n // Hatching: forty-five degrees, sliding slowly, clipped to the inside of the\n // outline so the stroke always sits on clean ground.\n let spacing = max(4.0, res.y * 0.014 * clamp(params.b, 0.0, 3.0));\n let hatchPos = (px.x + px.y) * 0.70710678 + t * 6.0;\n let hatchD = abs(fract(hatchPos / spacing) - 0.5) * spacing;\n let hatchLine = bp_hairline(hatchD, max(1.2, spacing * 0.18));\n let interior = 1.0 - smoothstep(-weight * 0.5 - 1.2, -weight * 0.5 + 0.2, sd);\n let hatched = hatchLine * interior * step(0.001, params.b);\n col = mix(col, pencil, hatched * 0.85);\n\n // The outline: a stroke of `weight` pixels centred on the edge of the ink.\n let stroke = bp_hairline(abs(sd), weight);\n col = mix(col, ink, stroke);\n\n // --- the dimensions -------------------------------------------------------------\n let gap = res.y * 0.055;\n let tick = res.y * 0.012;\n var dims = 0.0;\n var faint = 0.0;\n\n // Width, along the bottom.\n let wy = fitMax.y + gap;\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(fitMin.x, wy), vec2f(fitMax.x, wy)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(fitMin.x, wy - tick), vec2f(fitMin.x, wy + tick)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(fitMax.x, wy - tick), vec2f(fitMax.x, wy + tick)), hair));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMin.x, fitMax.y + 3.0), vec2f(fitMin.x, wy - tick)), 1.0));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMax.x, fitMax.y + 3.0), vec2f(fitMax.x, wy - tick)), 1.0));\n\n // Height, up the right-hand side.\n let hx = fitMax.x + gap;\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(hx, fitMin.y), vec2f(hx, fitMax.y)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(hx - tick, fitMin.y), vec2f(hx + tick, fitMin.y)), hair));\n dims = max(dims, bp_hairline(bp_segment(px, vec2f(hx - tick, fitMax.y), vec2f(hx + tick, fitMax.y)), hair));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMax.x + 3.0, fitMin.y), vec2f(hx - tick, fitMin.y)), 1.0));\n faint = max(faint, bp_hairline(bp_segment(px, vec2f(fitMax.x + 3.0, fitMax.y), vec2f(hx - tick, fitMax.y)), 1.0));\n\n // The labels give the size of the part as drawn, in device pixels, set at\n // a glyph height that follows the canvas but never drops below legible.\n let size = fitMax - fitMin;\n let widthPx = u32(round(size.x));\n let heightPx = u32(round(size.y));\n let em = max(12.0, res.y / 44.0);\n\n // \"W: … px\" reads left to right, centred under the width line.\n let wLabelW = st_labelWidth(widthPx) * em;\n let wOrigin = vec2f((fitMin.x + fitMax.x) * 0.5 - wLabelW * 0.5, wy + tick + em * 0.15);\n dims = max(dims, st_label((px - wOrigin) / em, ST_GLYPH_W, widthPx));\n\n // \"H: … px\" is turned a quarter clockwise to read down the height line: its\n // baseline runs down the canvas and the tops of its letters face right.\n let hLabelW = st_labelWidth(heightPx) * em;\n let hTop = hx + tick + em * 0.15 + em;\n let hStart = (fitMin.y + fitMax.y) * 0.5 - hLabelW * 0.5;\n dims = max(dims, st_label(vec2f(px.y - hStart, hTop - px.x) / em, ST_GLYPH_H, heightPx));\n\n col = mix(col, ink, faint * 0.45);\n col = mix(col, ink, dims);\n\n // What the bloom may pick up: the drawn lines, never the mat or the hatch.\n let lit = clamp(stroke + dims + hairCross * 0.8, 0.0, 1.0);\n return vec4f(col * params.intensity, lit);\n}\n";
|
|
8
9
|
readonly bokeh: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// bokeh\n// ---------------------------------------------------------------------------\n\n// Bokeh — out-of-focus lights drifting through depth. Additive, soft-edged.\n\nfn bokehLayer(p : vec2f, depth : f32, t : f32) -> vec3f {\n let density = mix(2.5, 7.0, depth);\n let drift = vec2f(t * 0.06 * (1.0 - depth * 0.7), -t * 0.09 * (1.0 - depth * 0.5));\n let grid = p * density + drift * density;\n let base = floor(grid);\n let frac = fract(grid) - 0.5;\n\n var acc = vec3f(0.0);\n for (var y = -1; y <= 1; y = y + 1) {\n for (var x = -1; x <= 1; x = x + 1) {\n let neighbour = vec2f(f32(x), f32(y));\n let rnd = st_hash22(base + neighbour + depth * 91.7);\n if (rnd.x < 0.35) { continue; }\n\n let wobble = vec2f(sin(t * 0.5 + rnd.y * TAU), cos(t * 0.4 + rnd.x * TAU)) * 0.12;\n let centre = neighbour + rnd - 0.5 + wobble;\n let d = length(frac - centre);\n\n // A disc with a bright rim — how a real lens renders defocused points.\n let radius = (0.10 + rnd.y * 0.22) * params.a * mix(1.6, 0.5, depth);\n let disc = smoothstep(radius, radius * (1.0 - params.b), d);\n let rim = smoothstep(radius * (1.0 - params.b * 0.8), radius, d) * disc;\n\n let tint = st_palette(rnd.y);\n acc = acc + tint * (disc * 0.55 + rim * params.c) * mix(0.35, 1.0, 1.0 - depth);\n }\n }\n return acc;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n var col = mix(params.colorA.rgb * 0.10, vec3f(0.008, 0.008, 0.02), uv.y);\n for (var i = 0; i < 4; i = i + 1) {\n col = col + bokehLayer(p, f32(i) / 3.0, t) * params.d;\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
9
10
|
readonly caldera: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// caldera\n// ---------------------------------------------------------------------------\n\n// Caldera — a volcanic island at night, lit by its own veins.\n//\n// The island is the usual fbm-minus-falloff, but the light source is inside\n// it: the valleys of a ridged field read as lava channels radiating from the\n// crater, brightest at the pool and cooling as they run out. Everything else\n// — basalt, the sea, the surf line that draws the coast — is lit by that\n// glow and a little moonlight.\n\nfn terrain(p : vec2f) -> f32 {\n return st_fbm2(p * 2.1 + vec2f(3.1, 7.7), 4) - smoothstep(0.1, 1.1, length(p)) * 0.9;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale * 2.2;\n\n let h = terrain(p) - 0.14;\n let r = length(p);\n let craterR = 0.16 * params.a;\n\n // Moonlit sea: dark, but alive, and paler near the coast so the island\n // silhouette always reads.\n let swell = st_fbm2(p * 5.0 + vec2f(t * 0.1, -t * 0.07), 3);\n var col = vec3f(0.05, 0.07, 0.13) * (0.7 + swell * 0.6);\n col = col + vec3f(0.10, 0.13, 0.20) * exp(min(h, 0.0) * 9.0);\n\n let land = smoothstep(0.0, 0.015, h);\n\n // A thin surf line draws the coast in the dark.\n let coast = smoothstep(0.02, 0.0, abs(h)) * smoothstep(0.4, 0.8, st_fbm2(p * 12.0 + t * 0.25, 3));\n col = mix(col, vec3f(0.35, 0.42, 0.50), coast * 0.6);\n\n // Basalt: moon-grey shoulders, brighter with height so the cone shows.\n let rock = mix(vec3f(0.10, 0.10, 0.14), vec3f(0.24, 0.22, 0.27), smoothstep(0.0, 0.45, h))\n * (0.7 + 0.5 * st_fbm2(p * 7.0, 3));\n col = mix(col, rock, land);\n\n // Lava rivers: thin runs at fixed angles off the pool, meandering more\n // the further they fall, each pulsing outward — downhill — as it goes.\n // The angular multiplier is an integer, so the runs close over the seam.\n let ang = atan2(p.y, p.x);\n let meander = (st_fbm2(p * 2.6 + vec2f(7.7, 1.3), 3) - 0.5) * (2.2 + r * 4.5) * params.warp;\n let runs = abs(sin(ang * 3.0 + meander));\n let width = 0.22 * (0.4 + clamp(params.b, 0.0, 1.5)) / (1.0 + r * 2.5);\n let vein = smoothstep(width, width * 0.2, runs);\n let reach = smoothstep(craterR + mix(0.35, 1.0, clamp(params.b, 0.0, 1.5) / 1.5), craterR * 0.5, r);\n let pulse = 0.85 + 0.15 * sin(t * 1.1 + r * 5.0);\n let downhill = 0.7 + 0.3 * sin(r * 15.0 - t * 2.2 + meander);\n var lava = vein * reach * land * downhill;\n\n // The crater pool: a dark cooling crust floating on the bright melt.\n let crust = smoothstep(0.45, 0.75, st_fbm2(p * 9.0 + vec2f(0.0, t * 0.12), 3));\n let pool = smoothstep(craterR, craterR * 0.5, r) * (1.0 - crust * 0.55);\n lava = max(lava, pool);\n\n // Heat colours from the shared ramp: colorB the running orange, colorC\n // the white heart.\n let heat = st_palette(clamp(lava * 1.3, 0.0, 1.0));\n col = mix(col, heat, smoothstep(0.10, 0.45, lava));\n\n // The glow the mountain throws: veins light their own banks, the crater\n // lights the summit, and both tint the smoke of the night air.\n col = col + params.colorB.rgb * lava * lava * 1.4;\n col = col + params.colorB.rgb * exp(-abs(r - craterR * 0.7) * 9.0) * 0.25 * params.c * pulse;\n col = col + params.colorB.rgb * vein * reach * 0.22 * params.c;\n\n // Embers: a handful of soft sparks climbing off the crater, not pixels.\n for (var i = 0; i < 5; i = i + 1) {\n let fi = f32(i);\n let cycle = fract(t * (0.11 + st_hash11(fi) * 0.07) + st_hash11(fi * 3.7));\n let emberPos = vec2f(\n (st_hash11(fi * 7.3) - 0.5) * 0.5 + sin(t * 0.8 + fi * 2.2) * 0.06 + cycle * 0.3,\n -cycle * 0.9 + 0.1,\n );\n let fadeIn = smoothstep(0.0, 0.1, cycle) * smoothstep(1.0, 0.55, cycle);\n col = col + params.colorC.rgb * exp(-length(p - emberPos) * 90.0) * fadeIn * params.d * 0.8;\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
10
11
|
readonly caustics: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// caustics\n// ---------------------------------------------------------------------------\n\n// Caustics — light focused through a moving water surface onto a pool floor.\n\nfn causticField(p : vec2f, t : f32, sharpness : f32) -> f32 {\n var acc = 0.0;\n var q = p;\n // Stacked, counter-rotating cell fields approximate the focusing pattern.\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let rotated = st_rot(fi * 1.1 + t * 0.05 * (fi + 1.0)) * q;\n let wave = sin(rotated.x * 6.0 + t * (1.1 + fi * 0.3))\n + sin(rotated.y * 6.0 - t * (0.9 + fi * 0.2))\n + sin((rotated.x + rotated.y) * 4.0 + t * 0.7);\n acc = acc + abs(wave) / 3.0;\n q = q * 1.35;\n }\n let v = 1.0 - clamp(acc / 3.0, 0.0, 1.0);\n return pow(v, 1.0 + sharpness * 14.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n var p = st_centered(uv) * params.scale * 2.0;\n\n // Surface ripple displaces the floor lookup.\n let ripple = vec2f(st_fbm2(p * 1.5 + t * 0.1, 3), st_fbm2(p * 1.5 + 4.2 - t * 0.08, 3));\n p = p + (ripple - 0.5) * params.warp;\n\n let light = causticField(p, t, params.a);\n\n // Chromatic split — the three wavelengths focus at slightly different depths.\n let split = params.b * 0.03;\n let r = causticField(p + vec2f(split, 0.0), t, params.a);\n let b = causticField(p - vec2f(split, 0.0), t, params.a);\n\n let floorTone = mix(params.colorA.rgb, params.colorB.rgb, clamp(st_fbm2(p * 0.8, 3), 0.0, 1.0)) * 0.35;\n var col = floorTone;\n col = col + params.colorC.rgb * vec3f(r, light, b) * params.intensity * 2.2;\n\n // Depth falloff towards the pool edges.\n col = col * (0.55 + 0.45 * smoothstep(1.1, 0.1, length(st_centered(uv)) * params.c));\n\n return st_post(st_tonemap(col), uv);\n}\n";
|
|
11
|
-
readonly chladni: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time.\n//\n// A graph shader is not one pass but a whole render graph in a single module:\n// a displacement field, a normal pass, an instanced particle draw, an\n// Unreal-style bloom chain, and a present pass, each its own entry point. The\n// runtime (src/graph.ts) drives them with `draw()` and explicit entry names;\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure, bloom chain and camera are ported from the vgpu\n// `fft-ocean` (\"Particles ocean\") example and keep its behaviour: additive\n// particles into an HDR scene, five blurred octaves recombined, linear to\n// sRGB on the way out.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads.\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(2) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(3) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n@group(0) @binding(6) var t_scene : texture_2d<f32>;\n@group(0) @binding(7) var t_input : texture_2d<f32>;\n@group(0) @binding(8) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(9) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(10) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(11) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(12) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(13) var t_bloom : texture_2d<f32>;\n@group(0) @binding(14) var linearSampler : sampler;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// ---------------------------------------------------------------------------\n// chladni\n// ---------------------------------------------------------------------------\n\n// Chladni — a singing plate, and the sand that maps its silence.\n//\n// The height is a pair of square-plate standing waves, the classic\n// cos(mπx)cos(nπy) figures, beating in time. The aux channel is the inverse:\n// where the figure crosses zero the plate never moves, and that is where the\n// accent colour gathers — the same reason sand walks to the nodal lines of a\n// real vibrating plate. Sweep drifts the mode numbers continuously, so the\n// figure re-solves itself into the next one instead of cutting.\n\nfn chladniFigure(q : vec2f, m : f32, n : f32) -> f32 {\n // The difference of the two orderings is what draws the diagonals; the sum\n // alone would be a plain checkerboard of humps.\n return cos(m * PI * q.x) * cos(n * PI * q.y) - cos(n * PI * q.x) * cos(m * PI * q.y);\n}\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans roughly ±1 across the plate; scale zooms the figure.\n let q = p * (params.scale / (gu.world.x * 0.5));\n\n // Sweep walks both mode numbers on incommensurate clocks, so the figure\n // never repeats a loop the eye can catch.\n let m = params.a + params.d * (0.5 + 0.5 * sin(t * 0.21));\n let n = params.b + params.d * (0.5 + 0.5 * sin(t * 0.13 + 2.1));\n let figure = chladniFigure(q, m, n);\n\n // The plate rings: the whole figure oscillates through zero, which is what\n // separates a vibration from a relief map of one.\n var h = figure * 0.4 * cos(t * 2.6);\n\n // A travelling ripple from the driver at the centre, fading toward the rim.\n let r = length(q);\n h = h + params.warp * 0.18 * sin(r * 22.0 - t * 3.0) * exp(-r * 1.6);\n\n // Sand: bright exactly where the figure is silent. The vibration term does\n // not gate it — nodes hold still at every phase, so the lines hold steady\n // while the plate rings around them.\n let node = 1.0 - smoothstep(0.0, 0.22, abs(figure));\n let aux = clamp(node * params.c * 0.85, 0.0, 1.0);\n\n return vec4f(0.0, h, 0.0, aux);\n}\n";
|
|
12
|
+
readonly chladni: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the one function the flavour asks for. Two flavours so far:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// shadertown particles prelude — prepended (after the shared and graph\n// preludes) to every shader in src/shaders/graph/particles/.\n//\n// The passes: a displacement field, a normal pass, and an instanced particle\n// draw into the HDR scene the shared bloom chain then reads. The variant file\n// below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure and camera are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example: additive particles into an HDR scene under a\n// fixed cinematic camera.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// ---------------------------------------------------------------------------\n// chladni\n// ---------------------------------------------------------------------------\n\n// Chladni — a singing plate, and the sand that maps its silence.\n//\n// The height is a pair of square-plate standing waves, the classic\n// cos(mπx)cos(nπy) figures, beating in time. The aux channel is the inverse:\n// where the figure crosses zero the plate never moves, and that is where the\n// accent colour gathers — the same reason sand walks to the nodal lines of a\n// real vibrating plate. Sweep drifts the mode numbers continuously, so the\n// figure re-solves itself into the next one instead of cutting.\n\nfn chladniFigure(q : vec2f, m : f32, n : f32) -> f32 {\n // The difference of the two orderings is what draws the diagonals; the sum\n // alone would be a plain checkerboard of humps.\n return cos(m * PI * q.x) * cos(n * PI * q.y) - cos(n * PI * q.x) * cos(m * PI * q.y);\n}\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans roughly ±1 across the plate; scale zooms the figure.\n let q = p * (params.scale / (gu.world.x * 0.5));\n\n // Sweep walks both mode numbers on incommensurate clocks, so the figure\n // never repeats a loop the eye can catch.\n let m = params.a + params.d * (0.5 + 0.5 * sin(t * 0.21));\n let n = params.b + params.d * (0.5 + 0.5 * sin(t * 0.13 + 2.1));\n let figure = chladniFigure(q, m, n);\n\n // The plate rings: the whole figure oscillates through zero, which is what\n // separates a vibration from a relief map of one.\n var h = figure * 0.4 * cos(t * 2.6);\n\n // A travelling ripple from the driver at the centre, fading toward the rim.\n let r = length(q);\n h = h + params.warp * 0.18 * sin(r * 22.0 - t * 3.0) * exp(-r * 1.6);\n\n // Sand: bright exactly where the figure is silent. The vibration term does\n // not gate it — nodes hold still at every phase, so the lines hold steady\n // while the plate rings around them.\n let node = 1.0 - smoothstep(0.0, 0.22, abs(figure));\n let aux = clamp(node * params.c * 0.85, 0.0, 1.0);\n\n return vec4f(0.0, h, 0.0, aux);\n}\n";
|
|
12
13
|
readonly cloth: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// cloth\n// ---------------------------------------------------------------------------\n\n// Cloth — a sheet of silk pinned by its top edge, and the pointer is a hand.\n//\n// The simulation is Jakobsen's (2001): points remember where they were last\n// step instead of carrying a velocity, so integration is one subtraction and\n// constraints are enforced by simply moving points — no forces, no springs,\n// no stiffness explosion. Every point holds twelve distance constraints\n// (structural, shear, bend) and the solver is Jacobi rather than Gauss-Seidel\n// because thousands of points relax in parallel; ten sweeps a frame is\n// enough for fabric that stretches like cloth and not like rubber. The\n// pointer carries an invisible sphere the constraints drape around.\n//\n// Drawing it is the unusual part. There is no vertex pipeline here, so the\n// cloth rasterises itself: each cell of the grid splats its own little patch\n// of surface into a G-buffer with atomicMin — depth rides the high bits and\n// the cell and its local uv ride the low ones, so one atomic is both the\n// z-test and the write. The fragment stage rebuilds the exact point on the\n// sheet from the simulation buffers and shades it there: smooth normals, a\n// thread-direction sheen, and a shadow thrown on the wall behind.\n\nstruct SimUniforms {\n // Seconds this step advances the simulation. 0 redraws without moving.\n dt : f32,\n aspect : f32,\n // How far the pointer moved this frame, in uv per second.\n pointerVel : vec2f,\n // The raster the cloth draws itself into.\n rasterW : f32,\n rasterH : f32,\n // How awake the pointer's sphere is. It wakes the moment the hand moves\n // and falls asleep after a few still seconds, so an untouched canvas shows\n // a sheet, not a sheet with a mysterious dent where a cursor happens to be.\n grab : f32,\n}\n\n@group(0) @binding(1) var<uniform> sim : SimUniforms;\n\n// Cloth grid. Positions ping-pong through the Jacobi sweeps; `prev` is the\n// verlet memory of where each point was one step ago.\nconst NX : i32 = 64;\nconst NY : i32 = 44;\n\n@group(0) @binding(2) var<storage, read> ptsIn : array<vec4f>;\n@group(0) @binding(3) var<storage, read_write> ptsOut : array<vec4f>;\n@group(0) @binding(4) var<storage, read_write> prevPts : array<vec4f>;\n@group(0) @binding(5) var<storage, read_write> norms : array<vec4f>;\n@group(0) @binding(6) var<storage, read_write> gbuf : array<atomic<u32>>;\n\n// The fragment stage reads the same state through read-only bindings.\n@group(0) @binding(8) var<storage, read> ptsView : array<vec4f>;\n@group(0) @binding(9) var<storage, read> normsView : array<vec4f>;\n@group(0) @binding(10) var<storage, read> gbufView : array<u32>;\n\n// Rest dimensions of the sheet, in world units. y grows downward like uv.\nconst CLOTH_W : f32 = 1.16;\nconst CLOTH_H : f32 = 1.04;\nconst PIN_Y : f32 = -0.46;\nconst REST_X : f32 = CLOTH_W / f32(NX - 1);\nconst REST_Y : f32 = CLOTH_H / f32(NY - 1);\nconst DIAG : f32 = 1.4142135;\nconst EMPTY : u32 = 0xffffffffu;\n\nfn pinAt(col : i32) -> vec3f {\n return vec3f(-CLOTH_W * 0.5 + f32(col) * REST_X, PIN_Y, 0.0);\n}\n\n// A mild perspective: z away from the viewer shrinks, z toward grows.\nfn project(p : vec3f) -> vec3f {\n let zoom = 0.82 * clamp(params.scale, 0.3, 2.5);\n let persp = 1.0 / (1.0 + p.z * 0.68);\n let s = p.xy * zoom * persp;\n return vec3f((0.5 + s.x / sim.aspect) * sim.rasterW, (0.5 + s.y) * sim.rasterH, p.z);\n}\n\n// Where the pointer sits on the cloth's plane, inverted from the projection.\nfn pointerWorld() -> vec3f {\n let zoom = 0.82 * clamp(params.scale, 0.3, 2.5);\n return vec3f(\n (params.pointer.x - 0.5) * sim.aspect / zoom,\n (params.pointer.y - 0.5) / zoom,\n 0.0,\n );\n}\n\nfn windAt(p : vec3f, t : f32) -> vec3f {\n // Two rates: a slow breathe that swings the whole sheet, and finer gusts\n // that ripple across it. Most of the wind blows in z — the billow is what\n // reads as depth.\n let breeze = 0.14 + params.warp * 0.2;\n let gust = params.warp;\n let n1 = st_fbm3(vec3f(p.x * 1.4, p.y * 1.4, t * 0.35), 2) - 0.5;\n let n2 = st_fbm3(vec3f(p.x * 2.3 + 11.0, p.y * 2.1, t * 0.8), 2) - 0.5;\n let slow = sin(t * 0.5 + p.y * 1.8) * 0.5 + 0.5;\n return vec3f(\n breeze * (0.4 + 0.6 * slow) + n2 * gust * 0.5,\n n1 * gust * 0.24,\n n1 * (gust * 1.2 + breeze * 0.6) + sin(t * 0.43 + p.x * 2.2) * breeze * 0.7,\n );\n}\n\n// --- passes ------------------------------------------------------------------\n\n@compute @workgroup_size(64)\nfn cs_integrate(@builtin(global_invocation_id) gid : vec3u) {\n let i = i32(gid.x);\n if (i >= NX * NY) { return; }\n let col = i % NX;\n let row = i / NX;\n\n // Read the verlet memory before overwriting it — this thread is the only\n // writer of its own slot, so the order within the function is the hazard.\n let p = ptsIn[i].xyz;\n let prev = prevPts[i].xyz;\n prevPts[i] = vec4f(p, 0.0);\n if (row == 0) {\n ptsOut[i] = vec4f(pinAt(col), 0.0);\n return;\n }\n\n let t = params.time * params.speed;\n let h = sim.dt;\n // params.d is Drape: heavy velvet at 2, weightless chiffon near 0.\n let gravity = vec3f(0.0, 0.75 + params.d * 0.8, 0.0);\n let acc = gravity + windAt(p, t) * (2.6 / max(0.4 + params.d, 0.4));\n\n let inertia = (p - prev) * 0.972;\n ptsOut[i] = vec4f(p + inertia + acc * h * h, 0.0);\n}\n\n@compute @workgroup_size(64)\nfn cs_constrain(@builtin(global_invocation_id) gid : vec3u) {\n let i = i32(gid.x);\n if (i >= NX * NY) { return; }\n let col = i % NX;\n let row = i / NX;\n\n if (row == 0) {\n ptsOut[i] = vec4f(pinAt(col), 0.0);\n return;\n }\n\n let p = ptsIn[i].xyz;\n var sum = vec3f(0.0);\n var weight = 0.0;\n\n // Twelve constraints in three rings. Structural holds the weave together,\n // shear stops it collapsing into a parallelogram, bend resists creasing.\n for (var k = 0; k < 12; k = k + 1) {\n var off : vec2i;\n var rest : f32;\n var stiff : f32;\n switch (k) {\n case 0: { off = vec2i(-1, 0); rest = REST_X; stiff = 1.0; }\n case 1: { off = vec2i(1, 0); rest = REST_X; stiff = 1.0; }\n case 2: { off = vec2i(0, -1); rest = REST_Y; stiff = 1.0; }\n case 3: { off = vec2i(0, 1); rest = REST_Y; stiff = 1.0; }\n case 4: { off = vec2i(-1, -1); rest = length(vec2f(REST_X, REST_Y)); stiff = 0.82; }\n case 5: { off = vec2i(1, -1); rest = length(vec2f(REST_X, REST_Y)); stiff = 0.82; }\n case 6: { off = vec2i(-1, 1); rest = length(vec2f(REST_X, REST_Y)); stiff = 0.82; }\n case 7: { off = vec2i(1, 1); rest = length(vec2f(REST_X, REST_Y)); stiff = 0.82; }\n case 8: { off = vec2i(-2, 0); rest = REST_X * 2.0; stiff = 0.45; }\n case 9: { off = vec2i(2, 0); rest = REST_X * 2.0; stiff = 0.45; }\n case 10: { off = vec2i(0, -2); rest = REST_Y * 2.0; stiff = 0.45; }\n default: { off = vec2i(0, 2); rest = REST_Y * 2.0; stiff = 0.45; }\n }\n let nc = col + off.x;\n let nr = row + off.y;\n if (nc < 0 || nc >= NX || nr < 0 || nr >= NY) { continue; }\n let q = ptsIn[nr * NX + nc].xyz;\n let d = p - q;\n let len = max(length(d), 0.00001);\n // Each endpoint owns half the error; Jacobi applies the average below.\n sum = sum + d * ((rest - len) / len) * 0.5 * stiff;\n weight = weight + stiff;\n }\n\n // Jacobi needs over-relaxation to converge in a dozen sweeps — 2.2 over\n // the summed stiffness is the strongest value that stays stable here.\n var next = p + sum * (2.2 / max(weight, 1.0));\n\n // The pointer's sphere. Pure projection — the constraints above turn the\n // shove into folds that travel across the whole sheet.\n let sphereR = (0.05 + params.c * 0.13) * sim.grab;\n let toP = next - pointerWorld();\n let dist = length(toP);\n if (dist < sphereR) {\n next = pointerWorld() + toP * (sphereR / max(dist, 0.0001));\n // A moving hand drags as well as blocks — a whisper of it per sweep.\n next = next + vec3f(sim.pointerVel * sim.dt * 0.1, 0.0);\n }\n\n ptsOut[i] = vec4f(next, 0.0);\n}\n\n@compute @workgroup_size(64)\nfn cs_normals(@builtin(global_invocation_id) gid : vec3u) {\n let i = i32(gid.x);\n if (i >= NX * NY) { return; }\n let col = i % NX;\n let row = i / NX;\n let l = ptsIn[row * NX + max(col - 1, 0)].xyz;\n let r = ptsIn[row * NX + min(col + 1, NX - 1)].xyz;\n let b = ptsIn[max(row - 1, 0) * NX + col].xyz;\n let t = ptsIn[min(row + 1, NY - 1) * NX + col].xyz;\n norms[i] = vec4f(normalize(cross(r - l, t - b)), 0.0);\n}\n\n@compute @workgroup_size(256)\nfn cs_clear(@builtin(global_invocation_id) gid : vec3u) {\n let cells = u32(sim.rasterW) * u32(sim.rasterH);\n if (gid.x >= cells) { return; }\n atomicStore(&gbuf[gid.x], EMPTY);\n}\n\n// One thread per cell of the cloth: estimate the patch's footprint on the\n// raster and splat enough samples to cover it without holes. Each sample is\n// one atomicMin: twelve depth bits up top so nearer wins, then the cell and\n// its local uv, so the winning write already carries everything the fragment\n// stage needs — no second pass, and no bit-exact recomputation to trust.\n@compute @workgroup_size(64)\nfn cs_raster(@builtin(global_invocation_id) gid : vec3u) {\n let cell = i32(gid.x);\n if (cell >= (NX - 1) * (NY - 1)) { return; }\n let cx = cell % (NX - 1);\n let cy = cell / (NX - 1);\n let p00 = ptsIn[cy * NX + cx].xyz;\n let p10 = ptsIn[cy * NX + cx + 1].xyz;\n let p01 = ptsIn[(cy + 1) * NX + cx].xyz;\n let p11 = ptsIn[(cy + 1) * NX + cx + 1].xyz;\n\n let s00 = project(p00);\n let s10 = project(p10);\n let s01 = project(p01);\n let s11 = project(p11);\n\n let lo = min(min(s00.xy, s10.xy), min(s01.xy, s11.xy));\n let hi = max(max(s00.xy, s10.xy), max(s01.xy, s11.xy));\n let span = hi - lo;\n // Nearly two samples per pixel of the widest axis: oversampled enough that\n // the bilinear patch leaves no gaps even mid-stretch, capped so a torn\n // cell cannot stall.\n let k = clamp(i32(max(span.x, span.y) * 1.9) + 1, 2, 36);\n\n let w = i32(sim.rasterW);\n let h = i32(sim.rasterH);\n for (var j = 0; j < k; j = j + 1) {\n for (var s = 0; s < k; s = s + 1) {\n let u = (f32(s) + 0.5) / f32(k);\n let v = (f32(j) + 0.5) / f32(k);\n let pos = mix(mix(p00, p10, u), mix(p01, p11, u), v);\n let scr = project(pos);\n let px = vec2i(scr.xy);\n if (px.x < 0 || px.x >= w || px.y < 0 || px.y >= h) { continue; }\n // 4094 rather than 4095, so a full G-buffer word can never collide\n // with the EMPTY sentinel.\n let depth = u32(clamp((scr.z + 2.0) * 1024.0, 0.0, 4094.0));\n let word = (depth << 20u) | (u32(cell) << 8u) | (u32(u * 15.99) << 4u) | u32(v * 15.99);\n atomicMin(&gbuf[px.y * w + px.x], word);\n }\n }\n}\n\n// --- the picture -------------------------------------------------------------\n\nfn coveredAt(px : vec2i) -> f32 {\n let w = i32(sim.rasterW);\n let h = i32(sim.rasterH);\n let c = vec2i(clamp(px.x, 0, w - 1), clamp(px.y, 0, h - 1));\n return select(1.0, 0.0, gbufView[c.y * w + c.x] == EMPTY);\n}\n\nfn backdrop(uv : vec2f) -> vec3f {\n // A wall behind the sheet, lit from the same corner as the cloth.\n let glow = 1.0 - length(vec2f((uv.x - 0.5) * sim.aspect, uv.y - 0.36)) * 0.9;\n return params.colorA.rgb * (0.16 + 0.3 * clamp(glow, 0.0, 1.0));\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let rs = vec2f(sim.rasterW, sim.rasterH);\n let pxf = uv * rs - 0.5;\n let base = vec2i(floor(pxf));\n let f = pxf - floor(pxf);\n\n // Coverage sampled bilinearly is the antialiasing: the silhouette fades\n // over one raster texel instead of stepping.\n let cov = mix(\n mix(coveredAt(base), coveredAt(base + vec2i(1, 0)), f.x),\n mix(coveredAt(base + vec2i(0, 1)), coveredAt(base + vec2i(1, 1)), f.x),\n f.y,\n );\n\n // The wall, with the sheet's shadow thrown down-right of it.\n var wall = backdrop(uv);\n let shadowPx = vec2i(pxf - vec2f(rs.x * -0.018, rs.y * -0.03));\n let shade = (coveredAt(shadowPx) + coveredAt(shadowPx + vec2i(2, 3)) + coveredAt(shadowPx - vec2i(2, 1))) / 3.0;\n wall = wall * (1.0 - shade * 0.45);\n\n if (cov < 0.001) {\n return st_post(st_tonemap(wall * params.intensity), uv);\n }\n\n // Take the G-buffer word of the nearest covered texel.\n var px = base;\n if (coveredAt(px) < 0.5) { px = base + vec2i(1, 0); }\n if (coveredAt(px) < 0.5) { px = base + vec2i(0, 1); }\n if (coveredAt(px) < 0.5) { px = base + vec2i(1, 1); }\n let w = i32(sim.rasterW);\n let word = gbufView[clamp(px.y, 0, i32(sim.rasterH) - 1) * w + clamp(px.x, 0, w - 1)];\n\n let cell = i32((word >> 8u) & 0xfffu);\n let cu = f32((word >> 4u) & 0xfu) / 15.0;\n let cv = f32(word & 0xfu) / 15.0;\n let cx = cell % (NX - 1);\n let cy = cell / (NX - 1);\n\n // Rebuild the exact point on the sheet from the simulation buffers — the\n // shading is computed on the true surface, not on raster texels.\n let i00 = cy * NX + cx;\n let n = normalize(mix(\n mix(normsView[i00].xyz, normsView[i00 + 1].xyz, cu),\n mix(normsView[i00 + NX].xyz, normsView[i00 + NX + 1].xyz, cu),\n cv,\n ));\n\n // The thread direction: the x axis laid onto the surface. Deriving it from\n // the smooth normal keeps the sheen continuous across cell boundaries,\n // where a per-cell edge vector would band.\n let tangent = normalize(vec3f(1.0, 0.0, 0.0) - n * n.x);\n\n let lightDir = normalize(vec3f(-0.45, -0.55, -0.7));\n let viewDir = vec3f(0.0, 0.0, -1.0);\n\n // Cloth is lit from both sides — a sheet has no inside.\n let diffuse = abs(dot(n, lightDir));\n\n // Kajiya-Kay: silk highlights run along the threads, not around a point.\n let th = dot(tangent, normalize(lightDir + viewDir));\n let sheen = pow(max(1.0 - th * th, 0.0), 48.0);\n\n // Grazing angles catch the light — the fresnel rim that reads as sheen.\n let rim = pow(1.0 - abs(dot(n, viewDir)), 2.6);\n\n // A steep diffuse curve: the folds go genuinely dark, which is where the\n // sense of weight comes from.\n var col = mix(params.colorA.rgb * 0.45, params.colorB.rgb, 0.10 + 0.90 * diffuse * diffuse);\n col = col + params.colorC.rgb * (sheen * 0.7 + rim * 0.45) * params.b;\n\n col = mix(wall, col, cov);\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
13
14
|
readonly cumulus: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// cumulus\n// ---------------------------------------------------------------------------\n\n// Cumulus — a sky of fair-weather cloud, shaded by one cheat.\n//\n// The clouds are fbm on a plane overhead, warped once so they cauliflower.\n// Their lighting is a single extra sample toward the sun: where density\n// falls off in that direction the cloud face is lit, where it rises the\n// face is in its own shadow. God rays are the same field again, sampled a\n// few times along the screen-space line to the sun.\n\nfn cloudDensity(q : vec2f, t : f32) -> f32 {\n let w = st_fbm2(q * 2.0 + vec2f(t * 0.03, 0.0), 3);\n return st_fbm2(q + (w - 0.5) * 0.8 + vec2f(t * 0.05, t * 0.012), 5);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv);\n let yUp = -p.y;\n\n let look = (params.pointer - vec2f(0.5)) * 0.15;\n\n let rd = normalize(vec3f(p.x + look.x * 0.2, yUp + 0.32 + look.y * 0.2, 1.0));\n let sunDir = normalize(vec3f(0.45, 0.16 + 0.4 * params.c, 0.65));\n let sunAmt = clamp(dot(rd, sunDir), 0.0, 1.0);\n\n // Sky: colorA overhead falling to a bright horizon, sun in colorC.\n var col = mix(mix(params.colorA.rgb, vec3f(1.0), 0.6), params.colorA.rgb, smoothstep(0.0, 0.4, rd.y));\n col = col + params.colorC.rgb * pow(sunAmt, 700.0) * 2.5;\n col = col + params.colorC.rgb * pow(sunAmt, 12.0) * 0.3;\n\n if (rd.y > 0.015) {\n // Project onto the cloud deck.\n let q = rd.xz / rd.y * 0.6 * params.scale;\n let fade = smoothstep(0.015, 0.14, rd.y);\n\n let den = cloudDensity(q, t);\n let cover = mix(0.72, 0.42, clamp(params.a, 0.0, 1.5) / 1.5);\n let body = smoothstep(cover, cover + 0.17 * params.b, den);\n\n // The one-sample cheat: density falling toward the sun means lit.\n let toward = cloudDensity(q + sunDir.xz * 0.14, t);\n let lit = clamp((den - toward) * 4.0 + 0.45, 0.0, 1.0);\n let shade = mix(params.colorB.rgb * 0.72, vec3f(1.03, 1.01, 0.98), lit);\n\n col = mix(col, shade, body * fade * 0.96);\n\n // Silver lining where a cloud edge crosses the sun's halo.\n let edge = smoothstep(cover, cover + 0.08, den) * (1.0 - smoothstep(cover + 0.1, cover + 0.3, den));\n col = col + params.colorC.rgb * edge * pow(sunAmt, 6.0) * 0.8 * fade;\n }\n\n // God rays: the cloud field re-read a few times along the line to the sun\n // in screen space — pure math, the \"mask\" is the same analytic function.\n let sunScreen = vec2f(0.32 - look.x * 0.3, 0.05 + 0.28 * params.c);\n var ray = 0.0;\n var weight = 0.55;\n let toSun = sunScreen - vec2f(p.x, yUp);\n for (var i = 1; i <= 8; i = i + 1) {\n let sp = vec2f(p.x, yUp) + toSun * (f32(i) / 8.0);\n let sr = normalize(vec3f(sp.x, sp.y + 0.32, 1.0));\n if (sr.y < 0.02) { continue; }\n let sq = sr.xz / sr.y * 0.6 * params.scale;\n let open = 1.0 - smoothstep(0.5, 0.72, cloudDensity(sq, t));\n ray = ray + open * weight;\n weight = weight * 0.82;\n }\n let towardSun = clamp(1.0 - length(toSun) * 0.9, 0.0, 1.0);\n col = col + params.colorC.rgb * ray * 0.05 * params.d * towardSun;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
14
15
|
readonly dither: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// dither\n// ---------------------------------------------------------------------------\n\n// Dither — a smooth field quantised through an 8x8 Bayer matrix.\n// Deliberately low-fi: flat colour, a visible pattern, no antialiasing.\n\n// Ordered-dither threshold, built by interleaving the bits of y and x^y.\nfn bayer8(coord : vec2f) -> f32 {\n let x = i32(coord.x) & 7;\n let y = i32(coord.y) & 7;\n var index = 0;\n for (var i = 0u; i < 3u; i = i + 1u) {\n let shift = 2u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return f32(index) / 64.0;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // Centred on 0.5 so every quantisation level gets used. Biasing the source\n // upwards instead just crushes the image to the top level.\n let field = st_fbm2(p * 2.0 + vec2f(t * 0.10, t * 0.06), 5);\n let sweep = clamp((field - 0.5) * 1.7 + (0.5 - uv.y) * params.c + 0.5, 0.0, 1.0);\n\n // Pixelate first, so the pattern reads the same at any resolution.\n let pixel = max(params.a, 1.0);\n let screenPos = floor(uv * params.resolution / pixel);\n let threshold = (bayer8(screenPos) - 0.5) / max(floor(params.b), 2.0);\n\n let levels = max(floor(params.b), 2.0);\n let stepped = min(floor(clamp(sweep + threshold * params.d * 2.0, 0.0, 1.0) * levels), levels - 1.0);\n let quantised = stepped / (levels - 1.0);\n\n let col = st_palette(quantised) * params.intensity;\n return st_post(col, uv);\n}\n";
|
|
15
|
-
readonly dunes: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time.\n//\n// A graph shader is not one pass but a whole render graph in a single module:\n// a displacement field, a normal pass, an instanced particle draw, an\n// Unreal-style bloom chain, and a present pass, each its own entry point. The\n// runtime (src/graph.ts) drives them with `draw()` and explicit entry names;\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure, bloom chain and camera are ported from the vgpu\n// `fft-ocean` (\"Particles ocean\") example and keep its behaviour: additive\n// particles into an HDR scene, five blurred octaves recombined, linear to\n// sRGB on the way out.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads.\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(2) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(3) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n@group(0) @binding(6) var t_scene : texture_2d<f32>;\n@group(0) @binding(7) var t_input : texture_2d<f32>;\n@group(0) @binding(8) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(9) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(10) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(11) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(12) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(13) var t_bloom : texture_2d<f32>;\n@group(0) @binding(14) var linearSampler : sampler;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// ---------------------------------------------------------------------------\n// dunes\n// ---------------------------------------------------------------------------\n\n// Dunes — a sand sea crawling downwind, crests picked out in light.\n//\n// The relief is ridged noise blended over soft fbm: the ridge operator folds\n// every octave around its midline, which is what turns rolling hills into\n// slip faces with a sharp crest. The whole field advects along the wind and\n// meanders under a domain warp, and a fine ripple rides the surface the way\n// wind ripples ride real dunes — perpendicular to the flow, an order of\n// magnitude smaller than the forms they sit on. Aux lands the accent colour\n// on the crests, where the sand is driest and catches the sun first.\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans a couple of noise cells across the frame; scale zooms the field.\n let q = p * (params.scale * 2.4 / gu.world.x);\n\n // The wind: the whole sand sea drifts, fast enough to read, slow enough\n // to stay a landscape.\n let wind = t * params.b * 0.05;\n var s = q + vec2f(wind, wind * 0.35);\n\n // Meander: the dune belts snake instead of running in ruler lines.\n let w = params.warp * 0.55;\n s = s + w * vec2f(\n st_fbm2(s * 1.3 + vec2f(0.0, wind * 0.5), 3) - 0.5,\n st_fbm2(s * 1.3 + vec2f(4.2, -wind * 0.5), 3) - 0.5,\n );\n\n let ridged = st_ridge2(s * 1.5, 4);\n let soft = st_fbm2(s * 1.5, 4);\n var h = mix(soft * 2.0 - 1.0, ridged * 1.9 - 0.85, clamp(params.a, 0.0, 1.0));\n\n // Wind ripples: a striped wave bent by noise so it breaks into fingers.\n let stripe = sin(s.x * 26.0 + s.y * 52.0 + st_noise2(s * 5.0) * 5.0);\n h = h + stripe * 0.05 * params.d;\n\n // Crests: the top of the ridge signal, sharpened so only the spine lights.\n let spine = clamp(ridged, 0.0, 1.0);\n let aux = clamp(spine * spine * spine * params.c * 0.9, 0.0, 1.0);\n\n return vec4f(0.0, h, 0.0, aux);\n}\n";
|
|
16
|
+
readonly dunes: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the one function the flavour asks for. Two flavours so far:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// shadertown particles prelude — prepended (after the shared and graph\n// preludes) to every shader in src/shaders/graph/particles/.\n//\n// The passes: a displacement field, a normal pass, and an instanced particle\n// draw into the HDR scene the shared bloom chain then reads. The variant file\n// below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure and camera are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example: additive particles into an HDR scene under a\n// fixed cinematic camera.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// ---------------------------------------------------------------------------\n// dunes\n// ---------------------------------------------------------------------------\n\n// Dunes — a sand sea crawling downwind, crests picked out in light.\n//\n// The relief is ridged noise blended over soft fbm: the ridge operator folds\n// every octave around its midline, which is what turns rolling hills into\n// slip faces with a sharp crest. The whole field advects along the wind and\n// meanders under a domain warp, and a fine ripple rides the surface the way\n// wind ripples ride real dunes — perpendicular to the flow, an order of\n// magnitude smaller than the forms they sit on. Aux lands the accent colour\n// on the crests, where the sand is driest and catches the sun first.\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans a couple of noise cells across the frame; scale zooms the field.\n let q = p * (params.scale * 2.4 / gu.world.x);\n\n // The wind: the whole sand sea drifts, fast enough to read, slow enough\n // to stay a landscape.\n let wind = t * params.b * 0.05;\n var s = q + vec2f(wind, wind * 0.35);\n\n // Meander: the dune belts snake instead of running in ruler lines.\n let w = params.warp * 0.55;\n s = s + w * vec2f(\n st_fbm2(s * 1.3 + vec2f(0.0, wind * 0.5), 3) - 0.5,\n st_fbm2(s * 1.3 + vec2f(4.2, -wind * 0.5), 3) - 0.5,\n );\n\n let ridged = st_ridge2(s * 1.5, 4);\n let soft = st_fbm2(s * 1.5, 4);\n var h = mix(soft * 2.0 - 1.0, ridged * 1.9 - 0.85, clamp(params.a, 0.0, 1.0));\n\n // Wind ripples: a striped wave bent by noise so it breaks into fingers.\n let stripe = sin(s.x * 26.0 + s.y * 52.0 + st_noise2(s * 5.0) * 5.0);\n h = h + stripe * 0.05 * params.d;\n\n // Crests: the top of the ridge signal, sharpened so only the spine lights.\n let spine = clamp(ridged, 0.0, 1.0);\n let aux = clamp(spine * spine * spine * params.c * 0.9, 0.0, 1.0);\n\n return vec4f(0.0, h, 0.0, aux);\n}\n";
|
|
16
17
|
readonly duotone: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// duotone\n// ---------------------------------------------------------------------------\n\n// Duotone — the layer's brightness read through your palette.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let src = st_srcRgb(uv);\n var l = st_luma(src);\n\n // Black and white point first, contrast about the midpoint after. In the\n // other order a hard contrast would clip before the range is set.\n l = clamp((l - params.a) / max(params.b - params.a, 0.01), 0.0, 1.0);\n l = clamp((l - 0.5) * max(params.c, 0.01) + 0.5, 0.0, 1.0);\n\n let mapped = st_palette(l);\n // Keeping some of the original chroma stops a busy photograph from turning\n // into a flat two-colour poster when that is not what you wanted.\n let col = mix(mapped, mix(mapped, src, 0.85), clamp(params.d, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
17
18
|
readonly edges: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// edges\n// ---------------------------------------------------------------------------\n\n// Edges — the layer reduced to its outlines.\n\nfn edges_luma(srcUv : vec2f) -> f32 {\n return st_luma(st_srcAt(srcUv).rgb);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let s = st_srcUv(uv);\n // Widening the step is what turns a hairline into a drawn line. Sub-texel\n // steps would just sample the same texel three times.\n let e = st_srcTexel() * max(params.a, 0.5);\n\n let tl = edges_luma(s + vec2f(-e.x, e.y));\n let tc = edges_luma(s + vec2f( 0.0, e.y));\n let tr = edges_luma(s + vec2f( e.x, e.y));\n let ml = edges_luma(s + vec2f(-e.x, 0.0));\n let mr = edges_luma(s + vec2f( e.x, 0.0));\n let bl = edges_luma(s + vec2f(-e.x, -e.y));\n let bc = edges_luma(s + vec2f( 0.0, -e.y));\n let br = edges_luma(s + vec2f( e.x, -e.y));\n\n // Sobel: the same kernel turned ninety degrees for the second axis.\n let gx = (tr + 2.0 * mr + br) - (tl + 2.0 * ml + bl);\n let gy = (tl + 2.0 * tc + tr) - (bl + 2.0 * bc + br);\n let gradient = length(vec2f(gx, gy));\n\n let line = smoothstep(params.b, params.b + 0.2, gradient);\n\n // The ground under the lines: the palette's dark end, with as much of the\n // original showing through as `fill` asks for.\n let ground = mix(st_palette(0.0), st_srcAt(s).rgb, clamp(params.c, 0.0, 1.0));\n var col = mix(ground, st_palette(1.0), line);\n\n // A little bleed off the line reads as ink, not as aliasing.\n col = col + st_palette(1.0) * pow(line, 3.0) * params.d;\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
18
19
|
readonly eyes: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// eyes\n// ---------------------------------------------------------------------------\n\n// Eyes — a wall of eyes that look where the pointer goes.\n//\n// Each cell is one eye: an almond of sclera, an iris that travels toward the\n// cursor, a lid that blinks on its own clock. The gaze is the interactive\n// part, but the idle frame carries itself — with no pointer the eyes wander,\n// each on its own phase, so a card is never a grid of dead centres.\n\n// Where this eye is looking, as an offset inside its socket.\nfn eyeGaze(id : vec2f, cursorDir : vec2f, cursorDist : f32, t : f32, follow : f32) -> vec2f {\n let seed = st_hash22(id);\n // Idle wander: a slow lissajous per eye, desynchronised by the hash.\n let wt = t * (0.5 + seed.x * 0.4) + seed.y * TAU;\n let wander = vec2f(cos(wt), sin(wt * 1.31)) * 0.12;\n\n // Tracking: full deflection quickly, so even a far cursor is clearly seen.\n let track = cursorDir * min(cursorDist * 0.8, 1.0) * 0.16;\n return mix(wander, track, follow);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let density = params.scale * 4.0;\n let p = st_centered(uv) * density;\n\n let id = floor(p);\n let f = fract(p) - 0.5;\n\n // The cursor in the same lattice space as the eyes. uv runs down the\n // screen and so does rendering, so no flip is needed for the gaze to\n // point the right way.\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5) * density;\n let toCursor = cursor - (id + vec2f(0.5));\n let cursorDist = length(toCursor);\n let cursorDir = toCursor / max(cursorDist, 0.0001);\n\n let seed = st_hash21(id);\n\n // Blink: a brief pulse on a per-eye clock. The squeeze narrows the almond\n // rather than sliding a lid, which is one smoothstep instead of a shape.\n let blinkPhase = fract(t * 0.13 * params.b + seed * 7.0);\n let closed = smoothstep(0.055, 0.0, abs(blinkPhase - 0.5));\n let open = (1.0 - closed) * (0.72 + 0.28 * seed);\n\n // Almond: the sclera's half-height as a function of x, pinched to zero at\n // the corners. Everything else is clipped against this.\n let halfWidth = 0.40;\n let xNorm = clamp(f.x / halfWidth, -1.0, 1.0);\n let lidHeight = open * 0.26 * sqrt(max(1.0 - xNorm * xNorm, 0.0));\n let aa = max(fwidth(f.y), 0.002);\n\n let inX = smoothstep(halfWidth + aa, halfWidth - aa, abs(f.x));\n let sclera = smoothstep(lidHeight + aa, lidHeight - aa, abs(f.y)) * inX;\n\n // Iris and pupil ride the gaze; both are clamped by the almond, so a look\n // to the side slides the iris under the lids the way a real eye does.\n let gaze = eyeGaze(id, cursorDir, cursorDist, t, params.a);\n let irisR = params.c * 0.16;\n let dIris = length(f - gaze);\n let iris = smoothstep(irisR + aa, irisR - aa, dIris) * sclera;\n let pupilR = irisR * clamp(params.d, 0.15, 0.9);\n let pupil = smoothstep(pupilR + aa, pupilR - aa, dIris) * sclera;\n\n // One fixed catchlight sells the sphere; it does not follow the gaze.\n let gleam = smoothstep(0.05, 0.0, length(f - gaze - vec2f(-0.05, -0.06))) * pupil;\n\n // Iris shading: darker at the rim, striated toward the pupil.\n let ring = clamp((dIris / max(irisR, 0.001)), 0.0, 1.0);\n let stria = 0.85 + 0.15 * sin(atan2(f.y - gaze.y, f.x - gaze.x) * 18.0 + seed * TAU);\n let irisTone = params.colorB.rgb * mix(1.15, 0.45, ring * ring) * stria;\n\n var col = params.colorA.rgb * (0.5 + 0.12 * st_hash21(id + 31.7));\n col = mix(col, params.colorC.rgb * (0.9 - 0.35 * abs(f.y) * 2.0), sclera);\n col = mix(col, irisTone, iris);\n col = mix(col, vec3f(0.02), pupil);\n col = mix(col, vec3f(1.0), gleam);\n\n // A soft shadow above each eye seats it in the wall.\n col = col * (1.0 - 0.25 * smoothstep(0.0, -0.3, f.y) * (1.0 - sclera));\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
19
|
-
readonly ferro: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time.\n//\n// A graph shader is not one pass but a whole render graph in a single module:\n// a displacement field, a normal pass, an instanced particle draw, an\n// Unreal-style bloom chain, and a present pass, each its own entry point. The\n// runtime (src/graph.ts) drives them with `draw()` and explicit entry names;\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure, bloom chain and camera are ported from the vgpu\n// `fft-ocean` (\"Particles ocean\") example and keep its behaviour: additive\n// particles into an HDR scene, five blurred octaves recombined, linear to\n// sRGB on the way out.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads.\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(2) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(3) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n@group(0) @binding(6) var t_scene : texture_2d<f32>;\n@group(0) @binding(7) var t_input : texture_2d<f32>;\n@group(0) @binding(8) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(9) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(10) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(11) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(12) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(13) var t_bloom : texture_2d<f32>;\n@group(0) @binding(14) var linearSampler : sampler;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// ---------------------------------------------------------------------------\n// ferro\n// ---------------------------------------------------------------------------\n\n// Ferro — a pool of ferrofluid, and the pointer is the magnet.\n//\n// The fluid rests as a low swell until the pointer's field reaches it: under\n// the cursor a crown of spikes rises, one cone per cell of a jittered\n// lattice, the hedgehog a real ferrofluid grows because the surface pays for\n// height with surface tension and settles on discrete spikes. The crown\n// follows the pointer live — this is the shader you move the mouse over.\n// Rings ripple outward from the same point, and the accent colour sits on\n// the spike tips where the field is strongest.\n\nfn ferroSpike(q : vec2f) -> f32 {\n // The nearest point of a jittered lattice, as a cone: exp of the squared\n // distance keeps the tip round and the flank steep, which is what reads\n // as fluid rather than as terrain.\n let cell = floor(q);\n var best = 99.0;\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let n = cell + vec2f(f32(dx), f32(dy));\n let site = n + 0.2 + 0.6 * st_hash22(n);\n best = min(best, dot(q - site, q - site));\n }\n }\n return exp(-best * 9.0);\n}\n\n/**\n * Where the magnet sits.\n *\n * A pointer that has never moved reports the exact centre of the frame, and\n * this camera unprojects the centre to the horizon: the crown rises two\n * hundred world units out, where it reads as a smudge of light rather than as\n * the hedgehog the shader is named for. A card is drawn once with no pointer,\n * so that smudge was the whole preview.\n *\n * Rest it low in the frame instead, close enough to the camera to see. Only\n * the untouched centre is replaced — a pointer parked on the centre by hand\n * has moved through something else first, and the offset is a pixel wide, so\n * every position a reader can actually ask for is honoured exactly.\n */\nfn ferroMagnet() -> vec2f {\n let untouched = all(abs(params.pointer - vec2f(0.5)) < vec2f(1e-5));\n return st_screenOnPlane(select(params.pointer, vec2f(0.5, 0.76), untouched));\n}\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n let magnet = ferroMagnet();\n let toMagnet = p - magnet;\n let r = length(toMagnet);\n\n // The magnet's reach, in world units. Everything the pointer does happens\n // inside this envelope, so the rest of the pool stays a quiet surface.\n let reach = 30.0 + params.a * 40.0;\n let field = exp(-(r * r) / (reach * reach));\n\n // The resting pool: a slow swell, so the fluid reads as liquid even when\n // the pointer is parked.\n let q = p * (params.scale * 3.0 / gu.world.x);\n var h = (st_fbm2(q * 1.6 + vec2f(t * 0.08, -t * 0.05), 3) - 0.5) * 0.5 * params.warp;\n\n // The crown: cones on a lattice, pulled up by the field. The lattice is\n // pinned to the world, not the pointer, so moving the magnet hands the\n // crown from one set of spikes to the next the way real fluid does.\n let spikeQ = p * (0.10 + params.b * 0.10);\n let cone = ferroSpike(spikeQ + vec2f(0.0, t * 0.02));\n let crown = cone * field * (0.6 + params.d * 1.6);\n h = h + crown;\n\n // Rings the disturbance sheds, dying with distance and with Ripples at 0.\n h = h + sin(r * 0.5 - t * 3.2) * exp(-r * 0.045) * 0.16 * params.c;\n\n // Spikes lean toward the magnet a little — the field lines are not vertical.\n let lean = select(-toMagnet / max(r, 1.0), vec2f(0.0), r < 1.0) * crown * 0.6;\n\n // Tips: the accent lands on the top of each cone, only inside the field.\n let aux = clamp(cone * cone * field * (0.7 + params.d * 0.5), 0.0, 1.0);\n\n return vec4f(lean.x, h, lean.y, aux);\n}\n";
|
|
20
|
+
readonly ferro: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the one function the flavour asks for. Two flavours so far:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// shadertown particles prelude — prepended (after the shared and graph\n// preludes) to every shader in src/shaders/graph/particles/.\n//\n// The passes: a displacement field, a normal pass, and an instanced particle\n// draw into the HDR scene the shared bloom chain then reads. The variant file\n// below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure and camera are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example: additive particles into an HDR scene under a\n// fixed cinematic camera.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// ---------------------------------------------------------------------------\n// ferro\n// ---------------------------------------------------------------------------\n\n// Ferro — a pool of ferrofluid, and the pointer is the magnet.\n//\n// The fluid rests as a low swell until the pointer's field reaches it: under\n// the cursor a crown of spikes rises, one cone per cell of a jittered\n// lattice, the hedgehog a real ferrofluid grows because the surface pays for\n// height with surface tension and settles on discrete spikes. The crown\n// follows the pointer live — this is the shader you move the mouse over.\n// Rings ripple outward from the same point, and the accent colour sits on\n// the spike tips where the field is strongest.\n\nfn ferroSpike(q : vec2f) -> f32 {\n // The nearest point of a jittered lattice, as a cone: exp of the squared\n // distance keeps the tip round and the flank steep, which is what reads\n // as fluid rather than as terrain.\n let cell = floor(q);\n var best = 99.0;\n for (var dy = -1; dy <= 1; dy = dy + 1) {\n for (var dx = -1; dx <= 1; dx = dx + 1) {\n let n = cell + vec2f(f32(dx), f32(dy));\n let site = n + 0.2 + 0.6 * st_hash22(n);\n best = min(best, dot(q - site, q - site));\n }\n }\n return exp(-best * 9.0);\n}\n\n/**\n * Where the magnet sits.\n *\n * A pointer that has never moved reports the exact centre of the frame, and\n * this camera unprojects the centre to the horizon: the crown rises two\n * hundred world units out, where it reads as a smudge of light rather than as\n * the hedgehog the shader is named for. A card is drawn once with no pointer,\n * so that smudge was the whole preview.\n *\n * Rest it low in the frame instead, close enough to the camera to see. Only\n * the untouched centre is replaced — a pointer parked on the centre by hand\n * has moved through something else first, and the offset is a pixel wide, so\n * every position a reader can actually ask for is honoured exactly.\n */\nfn ferroMagnet() -> vec2f {\n let untouched = all(abs(params.pointer - vec2f(0.5)) < vec2f(1e-5));\n return st_screenOnPlane(select(params.pointer, vec2f(0.5, 0.76), untouched));\n}\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n let magnet = ferroMagnet();\n let toMagnet = p - magnet;\n let r = length(toMagnet);\n\n // The magnet's reach, in world units. Everything the pointer does happens\n // inside this envelope, so the rest of the pool stays a quiet surface.\n let reach = 30.0 + params.a * 40.0;\n let field = exp(-(r * r) / (reach * reach));\n\n // The resting pool: a slow swell, so the fluid reads as liquid even when\n // the pointer is parked.\n let q = p * (params.scale * 3.0 / gu.world.x);\n var h = (st_fbm2(q * 1.6 + vec2f(t * 0.08, -t * 0.05), 3) - 0.5) * 0.5 * params.warp;\n\n // The crown: cones on a lattice, pulled up by the field. The lattice is\n // pinned to the world, not the pointer, so moving the magnet hands the\n // crown from one set of spikes to the next the way real fluid does.\n let spikeQ = p * (0.10 + params.b * 0.10);\n let cone = ferroSpike(spikeQ + vec2f(0.0, t * 0.02));\n let crown = cone * field * (0.6 + params.d * 1.6);\n h = h + crown;\n\n // Rings the disturbance sheds, dying with distance and with Ripples at 0.\n h = h + sin(r * 0.5 - t * 3.2) * exp(-r * 0.045) * 0.16 * params.c;\n\n // Spikes lean toward the magnet a little — the field lines are not vertical.\n let lean = select(-toMagnet / max(r, 1.0), vec2f(0.0), r < 1.0) * crown * 0.6;\n\n // Tips: the accent lands on the top of each cone, only inside the field.\n let aux = clamp(cone * cone * field * (0.7 + params.d * 0.5), 0.0, 1.0);\n\n return vec4f(lean.x, h, lean.y, aux);\n}\n";
|
|
20
21
|
readonly "film-grain": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// film-grain\n// ---------------------------------------------------------------------------\n\n// Film grain — a texture pass. Dark wash, heavy stock grain, halation, scanlines.\n\nfn wash(p : vec2f, t : f32) -> f32 {\n return st_fbm2(p * 1.2 + vec2f(t * 0.05, t * 0.03), 4);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale;\n\n // Chromatic aberration: read the same wash at three lateral offsets.\n let ca = params.c * 0.02;\n let wr = wash(p + vec2f(ca, 0.0), t);\n let wg = wash(p, t);\n let wb = wash(p - vec2f(ca, 0.0), t);\n\n var col = vec3f(\n st_palette(clamp(wr * 1.3 - 0.1, 0.0, 1.0)).r,\n st_palette(clamp(wg * 1.3 - 0.1, 0.0, 1.0)).g,\n st_palette(clamp(wb * 1.3 - 0.1, 0.0, 1.0)).b\n ) * params.intensity;\n\n // Stock grain, sampled per cell so it stays the same size at any DPR.\n let cell = floor(uv * params.resolution / max(params.d, 0.5));\n let n1 = st_hash21(cell + floor(t * 24.0) * 17.13);\n let n2 = st_hash21(cell.yx + floor(t * 24.0) * 3.71);\n col = col + (n1 * 0.65 + n2 * 0.35 - 0.5) * params.a;\n\n // Scanlines.\n let lines = sin(uv.y * params.resolution.y * 1.5) * 0.5 + 0.5;\n col = col * (1.0 - lines * params.b);\n\n // Halation blooming out of the brightest areas.\n col = col + st_palette(1.0) * pow(clamp(st_luma(col), 0.0, 1.0), 4.0) * 0.4;\n\n return st_post(col, uv);\n}\n";
|
|
21
22
|
readonly flow: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// flow\n// ---------------------------------------------------------------------------\n\n// Flow — the layer pushed around by a slow, turbulent field.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) * params.scale * 2.5;\n\n // Two fbm fields sampled a long way apart stand in for a divergence-free\n // field. It is not one, but the eye reads the swirl and not the maths.\n let drift = vec2f(t * 0.05, t * 0.03);\n // Turbulence trades soft hills for sharp crests. Ridged noise gives the flow\n // filaments and stringy detail; smooth noise gives it broad, lazy currents.\n let turbulence = clamp(params.a, 0.0, 1.0);\n let fx = mix(st_fbm2(p + drift, 4), st_ridge2(p + drift, 3), turbulence);\n let fy = mix(\n st_fbm2(p + drift + vec2f(17.3, 9.1), 4),\n st_ridge2(p + drift + vec2f(17.3, 9.1), 3),\n turbulence,\n );\n var field = vec2f(fx, fy) - 0.5;\n\n // Curl the field so it circulates rather than pulling everything one way.\n field = mix(field, vec2f(-field.y, field.x), clamp(params.b, 0.0, 1.0));\n\n let shove = field * params.warp * 0.12;\n\n // Smearing along the flow, rather than in a circle, is what makes it look\n // stirred instead of merely blurred.\n let smear = params.c * 0.06;\n let step = normalize(field + vec2f(0.0001)) * smear;\n var col = vec3f(0.0);\n for (var i = 0; i < 5; i = i + 1) {\n let k = (f32(i) / 4.0 - 0.5);\n // Inset, so the field has material to push around at the border rather\n // than running off the picture and smearing the last row of texels.\n col = col + st_srcAt(st_srcUvInset(uv + shove + step * k, 0.1)).rgb;\n }\n col = col / 5.0;\n\n // Tint by how hard the field is pushing, so the fast water shows colour.\n let speed = clamp(length(field) * 2.0, 0.0, 1.0);\n col = mix(col, col * st_palette(speed) * 1.8, clamp(params.d, 0.0, 1.0));\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
22
23
|
readonly fluid: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// fluid\n// ---------------------------------------------------------------------------\n\n// Fluid — ink in water that answers the pointer.\n//\n// A real Navier–Stokes solve, the incompressible half of it: Stam's stable\n// fluids (1999) as GPU Gems ch. 38 lays it out. Velocity is advected through\n// itself semi-Lagrangian — walk back along the flow and take what you find\n// there, unconditionally stable at any speed. Eight warm-started Jacobi\n// sweeps then solve for the pressure whose gradient cancels the divergence,\n// which is the step that makes it water instead of smear: push somewhere and\n// the field routes around itself. Vorticity confinement feeds back the small\n// whirls the coarse grid keeps averaging away. The dye rides on top at a\n// finer grid than the velocity that carries it — the solve stays cheap while\n// the ink stays sharp. Dragging the pointer lays force and ink down along\n// the stroke; leave it still and the simulation breathes on its own.\n\nstruct SimUniforms {\n // Seconds this step advances the simulation. 0 redraws without moving.\n dt : f32,\n aspect : f32,\n // Velocity, pressure, divergence and curl live on this grid…\n simW : f32,\n simH : f32,\n // …and the dye on this finer one.\n dyeW : f32,\n dyeH : f32,\n // One splat per step, laid along the segment the pointer covered this\n // frame so a fast stroke draws a line, never a trail of beads. From and to\n // coincide for the idle plumes, which collapses the segment to a point.\n splatPos : vec2f,\n splatFrom : vec2f,\n splatVel : vec2f,\n // x is a position on the palette ramp, y a brightness, z scales the splat\n // radius (idle plumes land wider than a pointer stroke), w > 0 arms it.\n splatColor : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> sim : SimUniforms;\n\n// Every neighbour-reading pass ping-pongs: read the whole of one buffer,\n// write the whole of another. The splats ride inside the advection passes,\n// so nothing needs to work in place.\n@group(0) @binding(2) var<storage, read> velIn : array<vec2f>;\n@group(0) @binding(3) var<storage, read_write> velOut : array<vec2f>;\n@group(0) @binding(4) var<storage, read> pIn : array<f32>;\n@group(0) @binding(5) var<storage, read_write> pOut : array<f32>;\n@group(0) @binding(6) var<storage, read_write> divBuf : array<f32>;\n@group(0) @binding(7) var<storage, read_write> curlBuf : array<f32>;\n@group(0) @binding(8) var<storage, read> dyeIn : array<vec4f>;\n@group(0) @binding(9) var<storage, read_write> dyeOut : array<vec4f>;\n\n// Velocity survives on its own for a while; the 0.2 is tuned, not derived.\nconst VEL_DISSIPATION : f32 = 0.2;\n// How much of last frame's pressure seeds the next solve. Starting warm is\n// what lets eight iterations do the work of a hundred cold ones.\nconst PRESSURE_CARRY : f32 = 0.8;\n// A hard ceiling on field speed, in uv per second. Normal operation sits far\n// below it; it exists so a flung pointer at maximum Force cannot blow the\n// solve up.\nconst MAX_SPEED : f32 = 2.5;\n\nfn simSize() -> vec2i { return vec2i(i32(sim.simW), i32(sim.simH)); }\nfn dyeSize() -> vec2i { return vec2i(i32(sim.dyeW), i32(sim.dyeH)); }\n\nfn velAt(c : vec2i) -> vec2f {\n let s = simSize();\n let q = clamp(c, vec2i(0), s - 1);\n return velIn[q.y * s.x + q.x];\n}\n\nfn pressureAt(c : vec2i) -> f32 {\n let s = simSize();\n let q = clamp(c, vec2i(0), s - 1);\n return pIn[q.y * s.x + q.x];\n}\n\n// Bilinear sample of the velocity field at a uv position.\nfn sampleVel(uv : vec2f) -> vec2f {\n let s = vec2f(sim.simW, sim.simH);\n let px = uv * s - 0.5;\n let base = floor(px);\n let f = px - base;\n let b = vec2i(base);\n let v00 = velAt(b);\n let v10 = velAt(b + vec2i(1, 0));\n let v01 = velAt(b + vec2i(0, 1));\n let v11 = velAt(b + vec2i(1, 1));\n return mix(mix(v00, v10, f.x), mix(v01, v11, f.x), f.y);\n}\n\nfn dyeAt(c : vec2i) -> vec4f {\n let s = dyeSize();\n let q = clamp(c, vec2i(0), s - 1);\n return dyeIn[q.y * s.x + q.x];\n}\n\nfn sampleDye(uv : vec2f) -> vec4f {\n let s = vec2f(sim.dyeW, sim.dyeH);\n let px = uv * s - 0.5;\n let base = floor(px);\n let f = px - base;\n let b = vec2i(base);\n let d00 = dyeAt(b);\n let d10 = dyeAt(b + vec2i(1, 0));\n let d01 = dyeAt(b + vec2i(0, 1));\n let d11 = dyeAt(b + vec2i(1, 1));\n return mix(mix(d00, d10, f.x), mix(d01, d11, f.x), f.y);\n}\n\n// The gaussian a splat deposits, measured from the stroke segment rather\n// than a point, in aspect-corrected uv space.\nfn splatWeight(uv : vec2f, radius : f32) -> f32 {\n let scale = vec2f(sim.aspect, 1.0);\n let a = sim.splatFrom * scale;\n let d = sim.splatPos * scale - a;\n let p = uv * scale - a;\n let t = clamp(dot(p, d) / max(dot(d, d), 1e-7), 0.0, 1.0);\n let q = p - t * d;\n return exp(-dot(q, q) / max(radius, 0.00003));\n}\n\nfn clampSpeed(v : vec2f) -> vec2f {\n let speed = length(v);\n if (speed > MAX_SPEED) { return v * (MAX_SPEED / speed); }\n return v;\n}\n\n// --- passes, in the order they run each step ---------------------------------\n\n@compute @workgroup_size(8, 8)\nfn cs_advect_vel(@builtin(global_invocation_id) gid : vec3u) {\n let s = simSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let uv = (vec2f(c) + 0.5) / vec2f(s);\n let back = uv - velIn[c.y * s.x + c.x] * sim.dt;\n var v = sampleVel(back) / (1.0 + VEL_DISSIPATION * sim.dt);\n // The force splat rides here rather than in a pass of its own — the field\n // is already in hand, so the splat costs nothing.\n if (sim.splatColor.w > 0.0) {\n let radius = (0.0004 + params.a * params.a * 0.004) * 4.0 * sim.splatColor.z;\n v = v + sim.splatVel * params.c * splatWeight(uv, radius);\n }\n v = clampSpeed(v);\n // Free-slip walls: the flow may run along an edge but never through it.\n if (c.x == 0 || c.x == s.x - 1) { v.x = 0.0; }\n if (c.y == 0 || c.y == s.y - 1) { v.y = 0.0; }\n velOut[c.y * s.x + c.x] = v;\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_curl(@builtin(global_invocation_id) gid : vec3u) {\n let s = simSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let l = velAt(c + vec2i(-1, 0)).y;\n let r = velAt(c + vec2i(1, 0)).y;\n let b = velAt(c + vec2i(0, -1)).x;\n let t = velAt(c + vec2i(0, 1)).x;\n curlBuf[c.y * s.x + c.x] = 0.5 * ((r - l) - (t - b));\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_vorticity(@builtin(global_invocation_id) gid : vec3u) {\n let s = simSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let idx = c.y * s.x + c.x;\n // Push each cell toward the nearest strong whirl, scaled by its spin —\n // this puts back the curl the coarse grid dissipates.\n let l = abs(curlBuf[clamp(c.x - 1, 0, s.x - 1) + c.y * s.x]);\n let r = abs(curlBuf[clamp(c.x + 1, 0, s.x - 1) + c.y * s.x]);\n let b = abs(curlBuf[c.x + clamp(c.y - 1, 0, s.y - 1) * s.x]);\n let t = abs(curlBuf[c.x + clamp(c.y + 1, 0, s.y - 1) * s.x]);\n var force = 0.5 * vec2f(t - b, l - r);\n force = force / (length(force) + 0.0001);\n force = force * curlBuf[idx] * (params.warp * 22.0);\n velOut[idx] = clampSpeed(velIn[idx] + force * sim.dt);\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_divergence(@builtin(global_invocation_id) gid : vec3u) {\n let s = simSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let idx = c.y * s.x + c.x;\n let l = velAt(c + vec2i(-1, 0)).x;\n let r = velAt(c + vec2i(1, 0)).x;\n let b = velAt(c + vec2i(0, -1)).y;\n let t = velAt(c + vec2i(0, 1)).y;\n divBuf[idx] = 0.5 * ((r - l) + (t - b));\n // Seed the solve with a fraction of last frame's answer.\n pOut[idx] = pIn[idx] * PRESSURE_CARRY;\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_jacobi(@builtin(global_invocation_id) gid : vec3u) {\n let s = simSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let idx = c.y * s.x + c.x;\n let l = pressureAt(c + vec2i(-1, 0));\n let r = pressureAt(c + vec2i(1, 0));\n let b = pressureAt(c + vec2i(0, -1));\n let t = pressureAt(c + vec2i(0, 1));\n pOut[idx] = (l + r + b + t - divBuf[idx]) * 0.25;\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_gradient(@builtin(global_invocation_id) gid : vec3u) {\n let s = simSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let idx = c.y * s.x + c.x;\n let l = pressureAt(c + vec2i(-1, 0));\n let r = pressureAt(c + vec2i(1, 0));\n let b = pressureAt(c + vec2i(0, -1));\n let t = pressureAt(c + vec2i(0, 1));\n var v = velIn[idx] - 0.5 * vec2f(r - l, t - b);\n if (c.x == 0 || c.x == s.x - 1) { v.x = 0.0; }\n if (c.y == 0 || c.y == s.y - 1) { v.y = 0.0; }\n velOut[idx] = v;\n}\n\n@compute @workgroup_size(8, 8)\nfn cs_advect_dye(@builtin(global_invocation_id) gid : vec3u) {\n let s = dyeSize();\n let c = vec2i(gid.xy);\n if (c.x >= s.x || c.y >= s.y) { return; }\n let uv = (vec2f(c) + 0.5) / vec2f(s);\n let back = uv - sampleVel(uv) * sim.dt;\n // params.b is Fade: how quickly the ink thins back to water.\n var d = sampleDye(back) / (1.0 + params.b * sim.dt);\n // The ink splat rides here for the same reason the force one rides in\n // cs_advect_vel: the buffer is already open.\n if (sim.splatColor.w > 0.0) {\n let radius = (0.0004 + params.a * params.a * 0.004) * sim.splatColor.z;\n let ink = st_palette(sim.splatColor.x) * sim.splatColor.y;\n d = d + vec4f(ink, 0.0) * splatWeight(uv, radius);\n }\n // A ceiling on density: a pointer resting on one spot must not pile ink\n // past what the tonemap can spend.\n dyeOut[c.y * s.x + c.x] = min(d, vec4f(6.0));\n}\n\n// --- the picture -------------------------------------------------------------\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let d = sampleDye(uv);\n\n // The ink shades itself: treat its brightness as a height field and light\n // it from the upper left, which is what gives the billows a surface.\n let texel = 1.0 / vec2f(sim.dyeW, sim.dyeH);\n let lumL = st_luma(sampleDye(uv - vec2f(texel.x, 0.0)).rgb);\n let lumR = st_luma(sampleDye(uv + vec2f(texel.x, 0.0)).rgb);\n let lumB = st_luma(sampleDye(uv - vec2f(0.0, texel.y)).rgb);\n let lumT = st_luma(sampleDye(uv + vec2f(0.0, texel.y)).rgb);\n let n = normalize(vec3f(lumL - lumR, lumB - lumT, length(texel) * 1.6));\n let light = clamp(dot(n, normalize(vec3f(-0.4, 0.6, 0.8))) + 0.72, 0.72, 1.15);\n\n var col = d.rgb * light * params.intensity;\n\n // A four-tap halo around anything already bright — most of what reads as\n // premium in a fluid is the light leaking off the densest ink.\n let reach = texel * 11.0;\n var halo = vec3f(0.0);\n halo = halo + max(sampleDye(uv + vec2f(reach.x, 0.0)).rgb - 0.35, vec3f(0.0));\n halo = halo + max(sampleDye(uv - vec2f(reach.x, 0.0)).rgb - 0.35, vec3f(0.0));\n halo = halo + max(sampleDye(uv + vec2f(0.0, reach.y)).rgb - 0.35, vec3f(0.0));\n halo = halo + max(sampleDye(uv - vec2f(0.0, reach.y)).rgb - 0.35, vec3f(0.0));\n col = col + halo * 0.22 * params.intensity;\n\n // Deep water behind the ink, so the empty state has a colour of its own.\n let depth = 1.0 - length(vec2f((uv.x - 0.5) * 0.8, uv.y - 0.45));\n col = col + params.colorA.rgb * (0.05 + 0.1 * clamp(depth, 0.0, 1.0)) * (1.0 - st_luma(d.rgb) * 0.8);\n\n return st_post(st_tonemap(col), uv);\n}\n";
|
|
@@ -31,7 +32,7 @@ export declare const shaderSources: {
|
|
|
31
32
|
readonly lens: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// lens\n// ---------------------------------------------------------------------------\n\n// Lens — barrel distortion, colour fringing and soft corners.\n\n// Brown-Conrady, truncated after the second radial term. That is enough to\n// bend a straight line convincingly and cheap enough to run three times.\nfn lens_distort(p : vec2f, k : f32) -> vec2f {\n let r2 = dot(p, p);\n return p * (1.0 + k * r2 + k * k * r2 * r2 * 0.35);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let aspect = st_aspect();\n let k = params.warp * 0.6;\n\n // Barrel distortion throws the corners outside the frame, which would leave\n // a black border around every preview. Measure how far the corner moves and\n // pull the whole field back in by that much, so bending the picture does not\n // also shrink it. Pincushion pulls inwards on its own and needs no help,\n // hence the clamp at 1.\n let corner = vec2f(aspect, 1.0) * 0.5;\n let stretch = length(lens_distort(corner, k)) / max(length(corner), 0.0001);\n let fill = 1.0 / max(stretch, 1.0);\n\n // Centred and aspect-corrected, so the distortion is radial about the frame\n // rather than about the corner of a stretched uv square.\n let p = (uv - 0.5) * vec2f(aspect, 1.0) * fill / max(params.c, 0.05);\n\n // Each channel gets its own constant. That is what glass does, and it is why\n // the fringing is invisible in the middle and worst in the corners.\n let spread = params.b * 0.06;\n let toUv = vec2f(1.0 / aspect, 1.0);\n let centre = lens_distort(p, k) * toUv + 0.5;\n let red = lens_distort(p, k * (1.0 - spread)) * toUv + 0.5;\n let blue = lens_distort(p, k * (1.0 + spread)) * toUv + 0.5;\n\n // Corners sit off the focal plane, so they lose sharpness first.\n let falloff = clamp(length(p) * 1.5, 0.0, 1.0);\n let soft = params.a * falloff * falloff * 0.02;\n\n var col = vec3f(\n st_srcSoften(red, soft).r,\n st_srcSoften(centre, soft).g,\n st_srcSoften(blue, soft).b,\n );\n\n // Past the frame the sampler repeats its last row. Fade to black instead.\n col = col * st_srcInside(st_srcUv(centre));\n\n // Barrel distortion pulls light towards the middle, so the corners dim.\n col = col * (1.0 - params.d * falloff * falloff);\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
32
33
|
readonly "liquid-metal": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// liquid-metal\n// ---------------------------------------------------------------------------\n\n// Liquid metal — a warped height field lit like polished chrome.\n// The look lives in the banded environment reflection, not in the noise: keep\n// the field low-frequency, and let the bands do the work.\n\nfn height(p : vec2f, t : f32) -> f32 {\n var q = p;\n q = q + vec2f(st_fbm2(q * 0.9 + vec2f(t * 0.18, 0.0), 3),\n st_fbm2(q * 0.9 + vec2f(0.0, t * 0.15), 3)) * params.warp * 1.6;\n return st_fbm2(q * 1.1 + vec2f(0.0, t * 0.08), 3);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 2.2;\n let t = st_time();\n\n // Fixed sampling offset in field units: a resolution-derived epsilon makes\n // the surface look completely different in a card and in a hero.\n let eps = 0.012;\n let h = height(p, t);\n let hx = height(p + vec2f(eps, 0.0), t);\n let hy = height(p + vec2f(0.0, eps), t);\n\n let slope = params.a * 0.55;\n let n = normalize(vec3f((h - hx) / eps * slope, (h - hy) / eps * slope, 1.0));\n\n let lightDir = normalize(vec3f(cos(t * 0.35) * 0.7, sin(t * 0.35) * 0.7, 0.9));\n let viewDir = vec3f(0.0, 0.0, 1.0);\n let halfway = normalize(lightDir + viewDir);\n\n let diffuse = clamp(dot(n, lightDir), 0.0, 1.0);\n let spec = pow(clamp(dot(n, halfway), 0.0, 1.0), 16.0 + params.b * 180.0);\n let fresnel = pow(1.0 - clamp(dot(n, viewDir), 0.0, 1.0), 4.0);\n\n // Wide, smooth reflection bands. Reflecting the view around the normal and\n // reading a ramp off it is what separates chrome from rough plastic.\n let reflected = reflect(-viewDir, n);\n let band = reflected.y * 1.6 + reflected.x * 0.5 + t * 0.05;\n let env = st_palette(clamp(band * 0.5 + 0.5, 0.0, 1.0));\n\n // A second, tighter band set gives the hard highlight streaks of polish.\n let streak = pow(abs(sin(band * PI * 1.5)), 8.0 + params.b * 40.0);\n\n var col = env * (0.35 + 0.75 * diffuse) * params.c;\n col = col + st_palette(1.0) * streak * 0.6 * params.c;\n col = col + vec3f(1.0) * spec * params.intensity * 1.4;\n col = col + st_palette(0.9) * fresnel * params.d;\n\n return st_post(st_tonemap(col), uv);\n}\n";
|
|
33
34
|
readonly lumen: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// lumen\n// ---------------------------------------------------------------------------\n\n// Lumen — a lattice of dots, each one a lamp and a wall at the same time.\n//\n// The lattice is what makes this affordable. Every dot is the same size, so the\n// nearest one to any point is simply the nearest lattice node, and the distance\n// to the whole field collapses to `length(p - round(p)) - radius`. That is an\n// exact distance, not an estimate, so a ray crosses a dozen cells in a handful\n// of steps and the shadows it casts run the width of the frame.\n\nfn lumEmission(id : vec2f, t : f32, lit : f32) -> vec3f {\n let seed = st_hash21(id);\n // A wave crossing the lattice decides which dots are on. Each dot carries its\n // own phase, so the front arrives as a scatter rather than as a straight line.\n let wave = 0.5 + 0.5 * sin(dot(id, vec2f(0.31, 0.19)) - t * 1.6 + seed * TAU);\n let on = smoothstep(1.0 - lit, 1.0 - lit + 0.28, wave);\n return st_palette(fract(seed * 0.7 + t * 0.04)) * on * on * 2.6;\n}\n\n// Jimenez's interleaved gradient noise. Turning every pixel's fan by the same\n// angle bands the penumbrae into spokes, and the obvious fixes trade that for\n// something worse: white noise clumps, and a low-discrepancy sequence lays the\n// offset down in diagonal stripes. This one was built for exactly this job.\nfn lumIgn(pixel : vec2f) -> f32 {\n return fract(52.9829189 * fract(dot(pixel, vec2f(0.06711056, 0.00583715))));\n}\n\n// One ray through the field. It stops at the first dot it touches and brings\n// back that dot's light — which is nothing at all if the dot is dark.\nfn lumTrace(origin : vec2f, dir : vec2f, start : f32, t : f32, radius : f32, lit : f32, reach : f32) -> vec3f {\n var travel = start;\n for (var i = 0; i < 30; i = i + 1) {\n let p = origin + dir * travel;\n let id = round(p);\n let d = length(p - id) - radius;\n if (d < 0.006) {\n let fall = travel / max(reach, 0.05);\n return lumEmission(id, t, lit) / (1.0 + fall * fall);\n }\n travel = travel + max(d, 0.02);\n if (travel > 16.0) { break; }\n }\n return vec3f(0.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n // Lattice units: one unit is one cell, which is what lets `round` find the\n // nearest dot.\n let p = st_centered(uv) * params.scale * 11.0;\n\n let radius = clamp(params.b, 0.02, 0.45);\n let lit = clamp(params.c, 0.0, 1.0);\n let rays = i32(clamp(params.a, 3.0, 40.0));\n\n let id = round(p);\n let d = length(p - id) - radius;\n\n let cellId = floor(uv * params.resolution);\n let dither = lumIgn(cellId);\n let turn = dither * TAU;\n\n // Leave from just outside the dot the pixel is standing on, so a dot is lit\n // by its neighbours instead of by itself, and stagger the start so the fans\n // do not all clear the same dot at the same distance.\n let start = max(-d, 0.0) + 0.015 + dither * 0.02;\n\n var sum = vec3f(0.0);\n for (var i = 0; i < rays; i = i + 1) {\n // A third of a wedge of slop on top of the ordered offset. Full white\n // noise per ray throws the stratification away and the frame goes\n // sandy; this much only breaks the pattern's grip on the lattice.\n let slop = (st_hash21(cellId + f32(i) * 17.31) - 0.5) * 0.35;\n let angle = turn + TAU * (f32(i) + 0.5 + slop) / f32(rays);\n sum = sum + lumTrace(p, vec2f(cos(angle), sin(angle)), start, t, radius, lit, params.d);\n }\n let gathered = sum / f32(rays) * params.intensity;\n\n // The air carries the radiance straight; a dot shows its own light plus a\n // little of what reaches it, over a grey body so the dark ones stay visible.\n let body = params.colorA.rgb * 0.55 + lumEmission(id, t, lit) + gathered * 0.4;\n // One pixel wide whatever the lattice is scaled to. The dots are the only\n // hard edge here, so a fixed width would show as the density changed.\n let dotEdge = max(fwidth(d), 0.00001);\n let mask = 1.0 - smoothstep(-dotEdge, dotEdge, d);\n\n var col = mix(gathered, body, mask);\n\n return st_post(st_tonemap(col), uv);\n}\n";
|
|
34
|
-
readonly maelstrom: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time.\n//\n// A graph shader is not one pass but a whole render graph in a single module:\n// a displacement field, a normal pass, an instanced particle draw, an\n// Unreal-style bloom chain, and a present pass, each its own entry point. The\n// runtime (src/graph.ts) drives them with `draw()` and explicit entry names;\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure, bloom chain and camera are ported from the vgpu\n// `fft-ocean` (\"Particles ocean\") example and keep its behaviour: additive\n// particles into an HDR scene, five blurred octaves recombined, linear to\n// sRGB on the way out.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads.\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(2) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(3) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n@group(0) @binding(6) var t_scene : texture_2d<f32>;\n@group(0) @binding(7) var t_input : texture_2d<f32>;\n@group(0) @binding(8) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(9) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(10) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(11) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(12) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(13) var t_bloom : texture_2d<f32>;\n@group(0) @binding(14) var linearSampler : sampler;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// ---------------------------------------------------------------------------\n// maelstrom\n// ---------------------------------------------------------------------------\n\n// Maelstrom — a whirlpool seen from the rim, arms wound into the drain.\n//\n// The surface is polar: spiral arms are a sine of angle-plus-log-radius — a\n// logarithmic spiral, the curve every real vortex draws because the twist\n// tightens as the water speeds up toward the middle. A funnel term pulls the\n// centre down, churn noise roughens the water, and the particles are also\n// displaced sideways along the tangent, so the grid itself visibly circles\n// the drain rather than bobbing in place. Aux is froth: the accent colour\n// rides the arm crests and thickens toward the eye.\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans roughly ±1 across the frame; scale zooms the vortex.\n var q = p * (params.scale / (gu.world.x * 0.5));\n\n // The eye wanders a little; a vortex pinned to the exact centre reads as a\n // screensaver, one that drifts reads as weather.\n q = q - params.warp * 0.15 * vec2f(sin(t * 0.5), cos(t * 0.37));\n\n let r = max(length(q), 0.02);\n let theta = atan2(q.y, q.x);\n let tangent = vec2f(-q.y, q.x) / r;\n\n // Inner water turns faster — the clamp keeps the eye from spinning to noise.\n let spin = t * (0.55 + 0.9 / max(r, 0.25));\n\n // Log-spiral arms, fading toward the rim where the pull lets go.\n let phase = theta * params.a + log(r) * params.b * 3.0 - spin;\n let amp = exp(-r * 1.3);\n let wave = sin(phase);\n var h = wave * amp * 0.9;\n\n // The funnel: the reason the picture has a middle.\n h = h - params.d * 1.3 * exp(-r * r * 3.5);\n\n // Churn: broken water riding the arms, advected the same way they turn.\n let churnP = vec2f(r * 3.0, theta * 2.0 + log(r) * params.b * 3.0 - spin);\n let churn = st_fbm2(churnP * 2.0 + vec2f(0.0, t * 0.3), 3) - 0.5;\n h = h + churn * 0.5 * params.c;\n\n // The water moves: sideways along the tangent with the arm it rides, so\n // the dots orbit instead of standing on a waving floor.\n let drift = tangent * (0.35 * amp * cos(phase));\n\n // Froth: the forward face of each arm, thickening toward the eye. Kept\n // partial on purpose — full froth buries the water colour under white.\n let crest = smoothstep(0.45, 1.0, wave) * amp;\n let eye = smoothstep(0.4, 0.08, r) * 0.4;\n let aux = clamp((crest * 0.8 + eye) * (0.5 + params.c * 0.5), 0.0, 1.0);\n\n return vec4f(drift.x, h, drift.y, aux);\n}\n";
|
|
35
|
+
readonly maelstrom: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the one function the flavour asks for. Two flavours so far:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// shadertown particles prelude — prepended (after the shared and graph\n// preludes) to every shader in src/shaders/graph/particles/.\n//\n// The passes: a displacement field, a normal pass, and an instanced particle\n// draw into the HDR scene the shared bloom chain then reads. The variant file\n// below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure and camera are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example: additive particles into an HDR scene under a\n// fixed cinematic camera.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// ---------------------------------------------------------------------------\n// maelstrom\n// ---------------------------------------------------------------------------\n\n// Maelstrom — a whirlpool seen from the rim, arms wound into the drain.\n//\n// The surface is polar: spiral arms are a sine of angle-plus-log-radius — a\n// logarithmic spiral, the curve every real vortex draws because the twist\n// tightens as the water speeds up toward the middle. A funnel term pulls the\n// centre down, churn noise roughens the water, and the particles are also\n// displaced sideways along the tangent, so the grid itself visibly circles\n// the drain rather than bobbing in place. Aux is froth: the accent colour\n// rides the arm crests and thickens toward the eye.\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // q spans roughly ±1 across the frame; scale zooms the vortex.\n var q = p * (params.scale / (gu.world.x * 0.5));\n\n // The eye wanders a little; a vortex pinned to the exact centre reads as a\n // screensaver, one that drifts reads as weather.\n q = q - params.warp * 0.15 * vec2f(sin(t * 0.5), cos(t * 0.37));\n\n let r = max(length(q), 0.02);\n let theta = atan2(q.y, q.x);\n let tangent = vec2f(-q.y, q.x) / r;\n\n // Inner water turns faster — the clamp keeps the eye from spinning to noise.\n let spin = t * (0.55 + 0.9 / max(r, 0.25));\n\n // Log-spiral arms, fading toward the rim where the pull lets go.\n let phase = theta * params.a + log(r) * params.b * 3.0 - spin;\n let amp = exp(-r * 1.3);\n let wave = sin(phase);\n var h = wave * amp * 0.9;\n\n // The funnel: the reason the picture has a middle.\n h = h - params.d * 1.3 * exp(-r * r * 3.5);\n\n // Churn: broken water riding the arms, advected the same way they turn.\n let churnP = vec2f(r * 3.0, theta * 2.0 + log(r) * params.b * 3.0 - spin);\n let churn = st_fbm2(churnP * 2.0 + vec2f(0.0, t * 0.3), 3) - 0.5;\n h = h + churn * 0.5 * params.c;\n\n // The water moves: sideways along the tangent with the arm it rides, so\n // the dots orbit instead of standing on a waving floor.\n let drift = tangent * (0.35 * amp * cos(phase));\n\n // Froth: the forward face of each arm, thickening toward the eye. Kept\n // partial on purpose — full froth buries the water colour under white.\n let crest = smoothstep(0.45, 1.0, wave) * amp;\n let eye = smoothstep(0.4, 0.08, r) * 0.4;\n let aux = clamp((crest * 0.8 + eye) * (0.5 + params.c * 0.5), 0.0, 1.0);\n\n return vec4f(drift.x, h, drift.y, aux);\n}\n";
|
|
35
36
|
readonly magnet: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// magnet\n// ---------------------------------------------------------------------------\n\n// Magnet — the field of three charges, one of them riding the pointer.\n//\n// Nothing here is traced. The stream function of a point charge is its angle\n// and the potential is the log of its distance, so field lines are level sets\n// of one sum and equipotentials level sets of the other — the entire plot is\n// two sin() calls over closed forms. The continuity of the picture depends on\n// the charges being integers: sin(n * sum of atan2) survives the branch cuts\n// only when n times each charge is whole, which is why the pointer charge\n// slider steps by one.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) / max(params.scale, 0.2);\n\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5)\n / max(params.scale, 0.2);\n\n // Two fixed poles and the pointer. The pointer's charge is a control so it\n // can attract, repel, or sit out entirely at zero.\n let sep = 0.28;\n let poleA = vec2f(-sep, 0.0);\n let poleB = vec2f(sep, 0.0);\n let qC = round(clamp(params.c, -2.0, 2.0));\n\n let rA = p - poleA;\n let rB = p - poleB;\n let rC = p - cursor;\n let dA = max(length(rA), 0.015);\n let dB = max(length(rB), 0.015);\n let dC = max(length(rC), 0.015);\n\n // Stream function and potential, superposed.\n let psi = atan2(rA.y, rA.x) - atan2(rB.y, rB.x) + qC * atan2(rC.y, rC.x);\n let phi = log(dA) - log(dB) + qC * log(dC);\n\n // Field strength, for brightness: strong near a pole, dead in the middle\n // of nowhere. The E-field of a 2D charge falls off as 1/r.\n let eField = rA / (dA * dA) - rB / (dB * dB) + qC * rC / (dC * dC);\n let strength = length(eField);\n let energy = strength / (2.5 + strength);\n\n // Field lines flow along themselves: the phase moves with time, so the\n // lines read as current rather than as a printed diagram.\n let lineCount = max(round(params.a), 2.0);\n let lines = pow(0.5 + 0.5 * sin(psi * lineCount - t * 1.5 * params.b), 8.0);\n\n // Equipotentials stand still and stay faint — they are the grid the flow\n // is read against.\n let rings = pow(0.5 + 0.5 * sin(phi * 7.0), 12.0) * 0.35;\n\n var col = params.colorA.rgb * 0.10;\n col = col + st_palette(energy) * lines * energy * 2.4;\n col = col + params.colorB.rgb * rings * energy;\n\n // The poles themselves: hot cores, colour-coded by sign.\n col = col + params.colorC.rgb * exp(-dA * 24.0) * 1.4;\n col = col + params.colorB.rgb * exp(-dB * 24.0) * 1.4;\n let cursorTone = mix(params.colorB.rgb, params.colorC.rgb, step(0.0, qC));\n col = col + cursorTone * exp(-dC * 24.0) * 1.4 * abs(qC) * 0.5;\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
36
37
|
readonly meadow: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// meadow\n// ---------------------------------------------------------------------------\n\n// Meadow — a flower field at golden hour, five rows deep.\n//\n// Silhouettes, not portraits: every row is drawn as one tone against the\n// glowing sky, far rows barely darker than the haze and the nearest nearly\n// ink — the same trick a photographer gets free at this hour. Depth is faked\n// twice more: rows slide by their own parallax under the pointer, and rows\n// off the focal plane widen their antialiasing band, which at header size\n// reads as lens blur.\n\n// How much a row's silhouette covers this pixel: grass blades on a fine\n// grid, flowers on a coarse one, both leaning with the wind.\nfn rowMask(p : vec2f, seed : f32, sway : f32, aa : f32, density : f32) -> f32 {\n var mask = 0.0;\n\n // The ground itself rolls: without this every row is a straight comb.\n let ground = (st_fbm2(vec2f(p.x * 1.7, seed), 3) - 0.5) * 0.17;\n\n // Grass: three blades per cell, curved, tapering to nothing.\n let bladeW = 0.021 / density;\n let bladeCell = floor(p.x / bladeW);\n for (var k = -1; k <= 1; k = k + 1) {\n let id = bladeCell + f32(k);\n let rnd = st_hash22(vec2f(id * 0.37, seed));\n let bx = (id + rnd.x) * bladeW;\n let bh = (0.035 + rnd.y * 0.13) * (0.7 + 0.6 * st_hash11(id * 0.61 + seed));\n let yUp = -p.y - ground;\n let bend = (sway + (rnd.x - 0.5) * 0.4) * (yUp / bh) * (yUp / bh);\n let d = abs(p.x - bx - bend * bh) - 0.0035 * (1.0 - yUp / bh);\n let blade = smoothstep(aa, -aa, d) * step(0.0, yUp) * step(yUp, bh);\n mask = max(mask, blade);\n }\n\n // Flowers: sparser, taller, a stem with a petalled head.\n let cellW = 0.13 / density;\n let cellId = floor(p.x / cellW);\n for (var k = -1; k <= 1; k = k + 1) {\n let id = cellId + f32(k);\n let rnd = st_hash22(vec2f(id, seed + 4.7));\n if (rnd.x > 0.5) { continue; }\n\n let cx = (id + 0.2 + rnd.x * 0.6) * cellW;\n let sh = 0.07 + rnd.y * 0.14;\n let yUp = -p.y - ground;\n let lean = (sway * 1.4 + (rnd.y - 0.5) * 0.3) * pow(clamp(yUp / sh, 0.0, 1.0), 2.0);\n let sx = p.x - cx - lean * sh;\n\n let stem = smoothstep(aa, -aa, abs(sx) - 0.0028) * step(0.0, yUp) * step(yUp, sh);\n mask = max(mask, stem);\n\n // The head: a rose curve, so even the silhouette says flower.\n let head = vec2f(sx, yUp - sh);\n let hang = atan2(head.y, head.x);\n let petals = 5.0 + floor(rnd.x * 4.0);\n let headR = (0.020 + rnd.y * 0.016) * (0.55 + 0.45 * abs(cos(petals * hang * 0.5)));\n mask = max(mask, smoothstep(headR + aa, headR - aa, length(head)));\n }\n\n return mask;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv);\n let yUp = -p.y;\n\n let look = (params.pointer - vec2f(0.5)) * 0.1;\n\n // The sky is the light source: colorA above, dissolving into the sun's\n // pool of colorC just over the field.\n let sunPos = vec2f(0.22 - look.x * 0.25, 0.05);\n let sunDist = length(vec2f(p.x, yUp) - sunPos);\n var col = mix(params.colorC.rgb, params.colorA.rgb, smoothstep(-0.15, 0.55, yUp - sunPos.y * 0.5));\n col = col + params.colorC.rgb * exp(-sunDist * 2.6) * 0.55 * params.c;\n col = col + vec3f(1.0, 0.97, 0.88) * exp(-sunDist * 16.0) * 1.2 * params.c;\n col = col + vec3f(1.0) * smoothstep(0.05, 0.035, sunDist) * params.c;\n\n // Four rows, back to front. Each is one flat tone: haze up close to the\n // sky's own colour at the back, ink at the front.\n let ink = params.colorB.rgb * 0.16;\n for (var i = 0; i < 4; i = i + 1) {\n let fi = f32(i) / 3.0; // 0 far, 1 near\n let rise = mix(-0.02, -0.46, fi);\n\n let zoom = mix(2.2, 1.0, fi);\n let q = vec2f(\n (p.x + look.x * mix(0.1, 0.6, fi) + f32(i) * 2.13) * zoom * params.scale,\n (p.y - (-rise)) * zoom,\n );\n\n // Focus sits on the middle rows; blur grows toward both ends of depth.\n let aa = (0.0016 + abs(fi - 0.6) * 0.012 * params.d) * zoom;\n let sway = params.b * 0.1 * sin(t * 1.2 + fi * 2.1) + params.b * 0.05 * sin(t * 2.7 + fi * 5.0);\n\n let mask = rowMask(q, f32(i) * 17.3, sway, aa, params.a);\n\n // Rows near the sun catch its rim on their silhouettes.\n let tone = mix(mix(col, ink, 0.35 + 0.16 * fi), ink, fi * fi);\n let rimmed = tone + params.colorC.rgb * exp(-sunDist * 3.0) * 0.25 * (1.0 - fi);\n col = mix(col, rimmed, mask);\n }\n\n return st_post(st_tonemap(col * params.intensity), uv);\n}\n";
|
|
37
38
|
readonly melt: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// melt\n// ---------------------------------------------------------------------------\n\n// Melt — the layer runs downhill, and the pointer is the heat.\n//\n// The displacement reads upward: a melting pixel shows what used to be above\n// it, so the whole effect is sampling the source at a smaller y. How much\n// smaller is a column-coherent noise — coherent down each column, varied\n// across them — which is what makes drips instead of fog. The streak pass\n// resamples the climb and keeps the brightest tap, the way a run of paint\n// carries its highlight down with it.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let aspect = st_aspect();\n\n // The heat field: a global simmer plus the pointer's blowtorch. Melting\n // grows toward the bottom of the frame because runs accumulate.\n let toCursor = (uv - params.pointer) * vec2f(aspect, 1.0);\n let heat = params.a * 0.5 + exp(-dot(toCursor, toCursor) / max(params.d * params.d, 0.001)) * params.a;\n let gravityGain = smoothstep(0.0, 0.7, uv.y);\n\n // Drips: one noise per column family, drifting slowly so the runs crawl.\n let dripNoise = st_fbm2(vec2f(uv.x * params.b * 8.0, t * 0.3), 3);\n let drip = pow(dripNoise, 2.0) * heat * (0.3 + gravityGain);\n\n // A slight sideways wobble keeps the runs from being perfectly plumb.\n let wobble = (st_noise2(vec2f(uv.x * 20.0, uv.y * 4.0 - t * 0.5)) - 0.5) * params.warp * 0.02 * heat;\n\n let base = uv + vec2f(wobble, -drip * 0.25);\n var col = st_srcAt(st_srcUvInset(base, 0.06)).rgb;\n\n // Streaks: walk back up the drip path and keep the brightest thing seen.\n // Weighted toward the origin so the smear fades as it falls.\n var streak = col;\n for (var i = 1; i <= 6; i = i + 1) {\n let fi = f32(i) / 6.0;\n let tap = st_srcAt(st_srcUvInset(base + vec2f(0.0, -drip * 0.25 * fi), 0.06)).rgb;\n streak = max(streak, tap * (1.0 - fi * 0.5));\n }\n col = mix(col, streak, clamp(params.c, 0.0, 1.0) * smoothstep(0.05, 0.3, drip));\n\n // The melt line glows faintly where the drip gradient is steep — molten\n // edges, strongest under the torch.\n let glow = smoothstep(0.15, 0.6, drip) * heat * 0.3;\n col = col + params.colorC.rgb * glow;\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
@@ -48,7 +49,7 @@ export declare const shaderSources: {
|
|
|
48
49
|
readonly "pixel-rain": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// pixel-rain\n// ---------------------------------------------------------------------------\n\n// Pixel Rain — rain over a sleeping skyline, drawn on a virtual pixel grid.\n//\n// Three flat bands of night sky under a moon and a few fixed stars, two\n// ranks of hashed buildings — the far rank hazed, the near rank black with\n// a sparse lattice of windows whose lights toggle rarely — and per-column\n// raindrops that fall by whole cells per tick onto a shimmering street.\n// Nothing is blended; every pixel is one of a dozen stops.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n let cells = params.resolution / px;\n\n let tt = floor(st_time() * 10.0) / 10.0;\n\n // Night sky: three flat bands, faintest at the top.\n var col = params.colorA.rgb * 0.5;\n if (puv.y > 0.55) {\n col = params.colorA.rgb;\n } else if (puv.y > 0.3) {\n col = params.colorA.rgb * 0.72;\n }\n\n // A few fixed stars, blinking very slowly. Hash on the cell, so they sit\n // still while the rain falls through them.\n let star = st_hash21(cell * 0.73);\n if (star > 0.9975 && puv.y < 0.45) {\n col = mix(params.colorA.rgb, vec3f(0.9), 0.35 + 0.3 * step(0.5, fract(star * 100.0 + tt * 0.2)));\n }\n\n // The moon: a hard disc, one shade, one shadow cell ring.\n let moonPos = vec2f(0.30 * st_aspect(), -0.30);\n let moonDist = length((puv - vec2f(0.5)) * vec2f(st_aspect(), 1.0) - moonPos);\n if (moonDist < 0.055) {\n col = vec3f(0.87, 0.89, 0.90);\n // A crescent: the shadow side takes the sky back.\n if (length((puv - vec2f(0.5)) * vec2f(st_aspect(), 1.0) - moonPos - vec2f(0.022, -0.012)) < 0.048) {\n col = params.colorA.rgb * 0.72;\n }\n }\n\n // Two ranks of buildings. Blocks are hashed by column group; the far rank\n // is drawn first and hazed towards the sky.\n var nearTop = 2.0; // the near roofline this pixel falls under, for antennas\n for (var layer = 0; layer < 2; layer = layer + 1) {\n let fl = f32(layer); // 0 far, 1 near\n let blockW = floor(mix(5.0, 8.0, fl));\n let block = floor((cell.x + fl * 37.0) / blockW);\n let rand = st_hash11(block + fl * 101.0);\n let topRow = cells.y * (mix(0.34, 0.52, fl) + rand * 0.28);\n\n if (cell.y > topRow) {\n col = mix(params.colorA.rgb * 0.38, params.colorA.rgb * 0.10, fl);\n\n // Windows: a sparse lattice, dim on the far rank, lit on the near.\n let wx = i32(cell.x) % 3;\n let wy = i32(cell.y) % 4;\n if (wx == 1 && wy == 2 && cell.y > topRow + 2.0) {\n let room = vec2f(floor(cell.x / 3.0), floor(cell.y / 4.0) + fl * 53.0);\n // A room's light toggles rarely — someone getting up, not a strobe.\n let awake = st_hash21(room + floor(tt * 0.25) * 5.0);\n if (awake > 1.0 - 0.3 * params.c) {\n col = mix(params.colorC.rgb * 0.4, params.colorC.rgb, fl);\n }\n }\n }\n if (layer == 1) {\n nearTop = topRow;\n\n // An antenna on the taller blocks: a one-cell mast with a slow red\n // blink at the tip.\n let mastX = block * blockW + floor(blockW * 0.5);\n if (rand > 0.62 && cell.x == mastX && cell.y <= topRow && cell.y > topRow - 4.0) {\n col = params.colorA.rgb * 0.16;\n if (cell.y > topRow - 2.0 && fract(tt * 0.5 + rand) < 0.25) {\n col = vec3f(0.85, 0.25, 0.25);\n }\n }\n }\n }\n\n // Rain: each sheared column owns a few drops, falling whole cells per\n // tick — a bright head cell trailing a dimmer one. The shear is the wind.\n let column = floor(cell.x + cell.y * params.d);\n for (var drop = 0; drop < 2; drop = drop + 1) {\n let rand = st_hash11(column * 2.7 + f32(drop) * 17.9);\n if (rand < clamp(params.b, 0.0, 1.0)) {\n let speed = 1.6 + rand * 1.4;\n let head = fract(rand * 7.13 + f32(drop) * 0.41 - tt * speed);\n let rows = fract(puv.y - head);\n if (rows < 1.2 / cells.y) {\n col = mix(params.colorB.rgb, vec3f(1.0), 0.25);\n } else if (rows < 3.0 / cells.y) {\n col = mix(col, params.colorB.rgb, 0.75);\n }\n }\n }\n\n // The street: a wet band at the foot of the frame, shimmering where drops\n // land. The shimmer re-rolls per tick, which reads as splashes.\n if (cell.y > cells.y - 3.0) {\n col = params.colorA.rgb * 0.55;\n let splash = st_hash21(vec2f(cell.x, floor(tt * 10.0)));\n if (splash > 1.0 - 0.14 * params.b) {\n col = mix(params.colorB.rgb, vec3f(1.0), 0.35);\n }\n // The city's light pools faintly on the wet asphalt.\n let room = st_hash11(floor(cell.x / 3.0));\n if (room > 0.55 && i32(cell.x) % 3 == 1) {\n col = mix(col, params.colorC.rgb, 0.22 * params.c);\n }\n }\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
49
50
|
readonly "pixel-sea": "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// pixel-sea\n// ---------------------------------------------------------------------------\n\n// Pixel Sea — a 16-bit sea: dithered sky, low sun, and its path on the water.\n//\n// A proper seascape rather than abstract bands: horizon in the upper third,\n// perspective rows that pack tighter as they recede, and the sun's\n// reflection breaking into dashes on the swell. Everything is sampled at\n// cell centres and quantised to stops derived from the colour wells; the\n// clock ticks at a sprite-sheet rate so nothing moves by a fraction of a cell.\n\nfn bayer4(cell : vec2f) -> f32 {\n let x = i32(cell.x) & 3;\n let y = i32(cell.y) & 3;\n var index = 0;\n for (var i = 0u; i < 2u; i = i + 1u) {\n let shift = 1u - i;\n index = (index << 1u) | ((y >> shift) & 1);\n index = (index << 1u) | (((x ^ y) >> shift) & 1);\n }\n return (f32(index) + 0.5) / 16.0;\n}\n\n// Stops: 0 sky top, 1 sky horizon, 2 sun, 3 deep, 4 mid, 5 light, 6 glint —\n// derived from the three wells: colorA the sky, colorB the water, colorC the\n// sun, with the horizon warmed toward the sun the way haze warms it.\nfn seaRamp() -> array<vec3f, 7> {\n let sky = params.colorA.rgb;\n let water = params.colorB.rgb;\n let sun = params.colorC.rgb;\n var c : array<vec3f, 7>;\n c[0] = sky;\n c[1] = mix(sky, sun, 0.5) * 1.08;\n c[2] = sun;\n c[3] = water * vec3f(0.40, 0.44, 0.60);\n c[4] = water * vec3f(0.76, 0.79, 0.94);\n c[5] = mix(water, c[1], 0.42);\n c[6] = mix(sun, vec3f(1.0), 0.55);\n return c;\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let px = st_pixelSize(params.a);\n let cell = floor(uv * params.resolution / px);\n let puv = (cell + 0.5) * px / params.resolution;\n\n let tt = floor(st_time() * 8.0) / 8.0;\n let ramp = seaRamp();\n\n let horizon = 0.34;\n let aspect = st_aspect();\n let sunX = 0.5;\n let threshold = bayer4(cell) - 0.5;\n\n var col : vec3f;\n if (puv.y < horizon) {\n // Sky: dithered bands, brightest at the horizon.\n let skyT = puv.y / horizon;\n let band = clamp(floor(skyT * 4.0 + 0.5 + threshold * params.d), 0.0, 4.0);\n col = mix(ramp[0], ramp[1], band / 4.0);\n\n // The sun sits just above the water, a hard disc with a dithered halo.\n let sd = length((puv - vec2f(sunX, horizon - 0.09)) * vec2f(aspect, 1.0));\n if (sd < 0.055 * params.c) {\n col = ramp[2];\n } else if (sd < 0.055 * params.c * (1.0 + bayer4(cell + vec2f(1.0, 3.0)))) {\n col = mix(col, ramp[2], 0.8);\n }\n } else {\n // The sea. Distance from the horizon is the perspective proxy: rows\n // pack tighter and waves get finer as dist shrinks.\n let dist = puv.y - horizon;\n let z = 0.05 / max(dist, 0.004);\n let xw = (puv.x - 0.5) * aspect * z;\n\n // Each row of cells shears on its own phase — the displaced-scanline\n // trick — and a wave field decides which cells catch light.\n let shear = sin(cell.y * 0.9 + tt * 1.2) * (1.5 * px / params.resolution.x);\n let wave = st_noise2(vec2f((xw + shear * z * 40.0) * 2.4 * params.b, z * 1.7 * params.b - tt * 0.55));\n\n // Base water: three stops, lighter toward the horizon.\n let depthBand = clamp(floor(smoothstep(0.66, 0.0, dist) * 2.0 + 0.5 + threshold * params.d), 0.0, 2.0);\n col = ramp[u32(depthBand) + 3u];\n\n // Wave rows: cells the swell lifts take the next stop up.\n if (wave > 0.62 + threshold * 0.14) {\n col = ramp[u32(min(depthBand + 1.0, 2.0)) + 3u];\n }\n\n // The sun's path: a column that widens toward the viewer, its light\n // broken into dashes by the same swell.\n let halfWidth = 0.015 + 0.06 * dist;\n let inPath = abs(puv.x - sunX) / aspect < halfWidth * (0.7 + 0.6 * bayer4(cell + vec2f(2.0, 1.0)));\n if (inPath && wave > 0.52 + dist * 0.45 + threshold * 0.1) {\n col = ramp[2];\n }\n\n // Rare single-cell glints outside the path, re-rolled slowly.\n let glint = st_hash21(cell + floor(tt * 5.0) * 7.0);\n if (glint > 0.9965 && wave > 0.55) {\n col = ramp[6];\n }\n }\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
50
51
|
readonly plasma: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// plasma\n// ---------------------------------------------------------------------------\n\n// Plasma — stacked sine fields, the oldest trick in the demoscene.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale * 6.0;\n let t = st_time();\n\n var v = 0.0;\n v = v + sin(p.x * 1.1 + t);\n v = v + sin(p.y * 1.3 + t * 1.17);\n v = v + sin((p.x + p.y) * 0.8 + t * 0.83);\n v = v + sin(length(p * params.a) * 1.4 - t * 1.6);\n\n // Rotating secondary field adds the interference bloom.\n let q = st_rot(t * 0.15 * params.b) * p;\n v = v + sin(q.x * 0.9 - q.y * 1.4 + t * 0.6) * params.c;\n\n let band = v * 0.125 + 0.5 + t * 0.05;\n var col = st_paletteCyclic(band * params.d);\n\n // Contour lines pick out the field's iso-levels.\n let contour = abs(fract(v * 0.9) - 0.5) * 2.0;\n col = col * (1.0 - smoothstep(0.85, 1.0, contour) * 0.55);\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
51
|
-
readonly pulsar: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time.\n//\n// A graph shader is not one pass but a whole render graph in a single module:\n// a displacement field, a normal pass, an instanced particle draw, an\n// Unreal-style bloom chain, and a present pass, each its own entry point. The\n// runtime (src/graph.ts) drives them with `draw()` and explicit entry names;\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The variant file below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure, bloom chain and camera are ported from the vgpu\n// `fft-ocean` (\"Particles ocean\") example and keep its behaviour: additive\n// particles into an HDR scene, five blurred octaves recombined, linear to\n// sRGB on the way out.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads.\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(2) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(3) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n@group(0) @binding(6) var t_scene : texture_2d<f32>;\n@group(0) @binding(7) var t_input : texture_2d<f32>;\n@group(0) @binding(8) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(9) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(10) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(11) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(12) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(13) var t_bloom : texture_2d<f32>;\n@group(0) @binding(14) var linearSampler : sampler;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// ---------------------------------------------------------------------------\n// pulsar\n// ---------------------------------------------------------------------------\n\n// Pulsar — a breathing globe of particles that leans toward your cursor.\n//\n// The grid gives up being a floor entirely: columns become longitude, rows\n// become latitude, and every particle is lifted off the sea plane onto a\n// sphere hanging in the dark. The surface carries a slow relief of 3D noise\n// sampled on the sphere itself — no seams, because the noise never sees the\n// uv wrap — and bands of light sweep the latitudes like a lighthouse turned\n// inside out. The pointer is a gentle gravity: wherever the cursor sits, the\n// whole globe drifts that way and stretches into a soft teardrop, brightest\n// on the side being pulled.\n\n// The centre sits below the frame, so what shows is the upper hemisphere —\n// a planet rising into the picture rather than a ball floating in it.\nconst PULSAR_CENTER : vec3f = vec3f(0.0, -12.0, 38.0);\nconst PULSAR_RADIUS : f32 = 42.0;\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // Column → longitude (wrapping), row → latitude.\n let lon = (p.x / gu.world.x + 0.5) * TAU + t * (0.1 + params.d * 0.25);\n let lat = (p.y / gu.world.x + 0.5) * PI;\n let n = vec3f(sin(lat) * cos(lon), cos(lat), sin(lat) * sin(lon));\n\n let radius = PULSAR_RADIUS * params.scale;\n\n // Breath: the whole globe swells and settles.\n var r = radius * (1.0 + 0.05 * params.c * sin(t * 1.3));\n\n // Relief: noise on the sphere, drifting through itself, so the surface\n // boils slowly instead of scrolling.\n let relief = st_fbm3(n * 2.6 + vec3f(0.0, t * 0.1, 0.0), 4);\n r = r + (relief - 0.5) * radius * 0.28 * params.b;\n\n // The attraction: the point on the pointer's ray nearest the globe is\n // where the cursor \"is\" in the globe's space. The whole body drifts that\n // way, capped so it leans rather than leaves, and the surface facing the\n // pull stretches into a teardrop.\n let ray = st_pointerRay();\n let along = max(dot(PULSAR_CENTER - ray.origin, ray.dir), 0.0);\n let cursor = ray.origin + ray.dir * along;\n let delta = cursor - PULSAR_CENTER;\n let dist = length(delta);\n let pullDir = delta / max(dist, 0.001);\n // Full lean once the cursor is a couple of radii away; dead centre is rest.\n let lean = min(dist / (radius * 2.0), 1.0) * params.warp;\n let facing = max(dot(n, pullDir), 0.0);\n r = r + facing * facing * radius * 0.1 * lean;\n let drift = pullDir * radius * 0.12 * lean;\n\n // Bands: light sweeping the latitudes, sharpened to streaks, broken up by\n // the relief so they read as weather rather than as rings.\n let sweep = sin(lat * (3.0 + params.a * 5.0) + t * 1.6 + relief * 3.0);\n let bands = pow(clamp(sweep * 0.5 + 0.5, 0.0, 1.0), 6.0);\n\n // The pulled side glows, so the attraction is readable even mid-lean.\n // Capped low: sixty-five thousand additive splats share a small circle of\n // screen here, and full-strength aux would burn the core to white.\n let aux = clamp(bands * 0.45 + facing * facing * lean * 0.35 + (relief - 0.5) * 0.25, 0.0, 0.6);\n\n let shell = PULSAR_CENTER + drift + n * r;\n let s = gu.world.y;\n return vec4f(shell.x - p.x, shell.y, shell.z - p.y, aux) / vec4f(s, s, s, 1.0);\n}\n";
|
|
52
|
+
readonly pulsar: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown graph prelude — prepended (after the shared prelude) to every\n// graph shader at build time, whatever its flavour.\n//\n// A graph shader is not one pass but a whole render graph in a single module,\n// each pass its own entry point, and the runtime (src/graph.ts) drives them\n// with `draw()` and explicit entry names. This file carries the part every\n// flavour shares: the full-screen vertex stage, the present pass, and an\n// Unreal-style bloom chain that reads whatever HDR scene the flavour drew.\n// `effect()` picks the first vertex and fragment entries, which is why\n// `vs_fullscreen` and `fs_main` (the present pass) are declared first.\n//\n// The flavour prelude that follows this one (src/shaders/graph/<flavour>/)\n// declares the passes that draw the scene, and the variant file below that\n// supplies the one function the flavour asks for. Two flavours so far:\n//\n// particles a displacement field, normals, an instanced particle draw\n// drafting a source layer measured, distance-fielded and drawn as a plate\n//\n// Bindings are declared once for the whole module; vgpu builds layouts per\n// entry point, so each pass only owes the bindings it actually reads. The\n// shared passes bind from 20 upward, leaving 1 to 19 to the flavour — the\n// drafting flavour spends 1 to 3 on the source prelude.\n//\n// The bloom chain and present pass are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example and keep its behaviour: five blurred octaves\n// recombined, linear to sRGB on the way out.\n\nstruct BlurUniforms {\n direction : vec2f,\n invSize : vec2f,\n coeffs0 : vec4f,\n coeffs1 : vec4f,\n coeffs2 : vec4f,\n coeffs3 : vec4f,\n coeffs4 : vec4f,\n coeffs5 : vec4f,\n}\n\nstruct BloomUniforms {\n threshold : f32,\n smoothWidth : f32,\n strength : f32,\n radius : f32,\n factors0 : vec4f,\n factors1 : vec4f,\n}\n\n@group(0) @binding(20) var<uniform> blurU : BlurUniforms;\n@group(0) @binding(21) var<uniform> bloomU : BloomUniforms;\n@group(0) @binding(22) var t_scene : texture_2d<f32>;\n@group(0) @binding(23) var t_input : texture_2d<f32>;\n@group(0) @binding(24) var t_blur1 : texture_2d<f32>;\n@group(0) @binding(25) var t_blur2 : texture_2d<f32>;\n@group(0) @binding(26) var t_blur3 : texture_2d<f32>;\n@group(0) @binding(27) var t_blur4 : texture_2d<f32>;\n@group(0) @binding(28) var t_blur5 : texture_2d<f32>;\n@group(0) @binding(29) var t_bloom : texture_2d<f32>;\n@group(0) @binding(30) var linearSampler : sampler;\n\nstruct FullscreenOut {\n @builtin(position) position : vec4f,\n @location(0) uv : vec2f,\n}\n\n// Same triangle and uv orientation vgpu generates for a plain effect, so the\n// fullscreen fragment entries below read uv exactly like every other shader.\n@vertex fn vs_fullscreen(@builtin(vertex_index) vi : u32) -> FullscreenOut {\n var pos = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));\n var uv = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));\n var out : FullscreenOut;\n out.position = vec4f(pos[vi], 0.0, 1.0);\n out.uv = uv[vi];\n return out;\n}\n\n// --- present ---------------------------------------------------------------\n// First fragment entry on purpose: the runtime's `effect()` for this shader\n// resolves to it, and the Stage drives it like any other shader's fragment.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let scene = textureSample(t_scene, linearSampler, uv);\n let bloom = textureSample(t_bloom, linearSampler, uv);\n let linear = max(scene.rgb + bloom.rgb, vec3f(0.0));\n let lt = linear * 12.92;\n let gt = 1.055 * pow(linear, vec3f(0.41666)) - vec3f(0.055);\n let srgb = select(gt, lt, linear <= vec3f(0.0031308));\n return st_post(srgb, uv);\n}\n\n// --- bloom -------------------------------------------------------------------\n\nfn st_luminance(rgb : vec3f) -> f32 {\n return dot(rgb, vec3f(0.299, 0.587, 0.114));\n}\n\n@fragment fn fs_bright(@location(0) uv : vec2f) -> @location(0) vec4f {\n // LuminosityHighPassShader, as ported by the fft-ocean example.\n let texel = textureSample(t_scene, linearSampler, uv);\n let gate = smoothstep(bloomU.threshold, bloomU.threshold + bloomU.smoothWidth, st_luminance(texel.xyz));\n return mix(vec4f(vec3f(0.0), 0.0), texel, gate);\n}\n\nconst ST_BLUR_RADIUS : u32 = 22u;\n\nfn st_blurCoefficient(i : u32) -> f32 {\n let packed = array<vec4f, 6>(\n blurU.coeffs0, blurU.coeffs1, blurU.coeffs2,\n blurU.coeffs3, blurU.coeffs4, blurU.coeffs5,\n );\n return packed[i / 4u][i % 4u];\n}\n\n@fragment fn fs_blur(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass separable blur: coefficients past each level's radius are\n // zero, so one fixed loop serves all five levels.\n var weightSum = st_blurCoefficient(0u);\n var diffuseSum = textureSample(t_input, linearSampler, uv).rgb * weightSum;\n for (var i = 1u; i < ST_BLUR_RADIUS; i = i + 1u) {\n let w = st_blurCoefficient(i);\n let offset = blurU.direction * blurU.invSize * f32(i);\n let s1 = textureSample(t_input, linearSampler, uv + offset).rgb;\n let s2 = textureSample(t_input, linearSampler, uv - offset).rgb;\n diffuseSum = diffuseSum + (s1 + s2) * w;\n }\n return vec4f(diffuseSum, 1.0);\n}\n\nfn st_bloomFactor(i : u32) -> f32 {\n let v = array<f32, 8>(\n bloomU.factors0.x, bloomU.factors0.y, bloomU.factors0.z, bloomU.factors0.w,\n bloomU.factors1.x, bloomU.factors1.y, bloomU.factors1.z, bloomU.factors1.w,\n );\n let mirror = 1.2 - v[i];\n return mix(v[i], mirror, bloomU.radius);\n}\n\n@fragment fn fs_composite(@location(0) uv : vec2f) -> @location(0) vec4f {\n // UnrealBloomPass composite, tint colours all white.\n let bloom = 3.0 * bloomU.strength * (\n st_bloomFactor(0u) * textureSample(t_blur1, linearSampler, uv).rgb +\n st_bloomFactor(1u) * textureSample(t_blur2, linearSampler, uv).rgb +\n st_bloomFactor(2u) * textureSample(t_blur3, linearSampler, uv).rgb +\n st_bloomFactor(3u) * textureSample(t_blur4, linearSampler, uv).rgb +\n st_bloomFactor(4u) * textureSample(t_blur5, linearSampler, uv).rgb\n );\n return vec4f(bloom, max(bloom.r, max(bloom.g, bloom.b)));\n}\n\n// shadertown particles prelude — prepended (after the shared and graph\n// preludes) to every shader in src/shaders/graph/particles/.\n//\n// The passes: a displacement field, a normal pass, and an instanced particle\n// draw into the HDR scene the shared bloom chain then reads. The variant file\n// below this prelude supplies exactly one function:\n//\n// fn st_field(p : vec2f, t : f32) -> vec4f\n//\n// `p` is the world-space position of a grid cell (±worldSize/2), `t` is\n// `st_time()`. The return value is displacement xyz in field units (scaled to\n// world units by `gu.world.y`) plus an aux channel in w — the shader's own\n// meaning of \"foam\": whatever the accent colour should land on.\n//\n// The pass structure and camera are ported from the vgpu `fft-ocean`\n// (\"Particles ocean\") example: additive particles into an HDR scene under a\n// fixed cinematic camera.\n\nstruct GraphUniforms {\n view : mat4x4f,\n projection : mat4x4f,\n // canvas width, canvas height, point scale, grid resolution\n viewport : vec4f,\n // world size, height scale, point size, texel spacing (world / grid)\n world : vec4f,\n // fade near, fade far, fade power, unused\n fade : vec4f,\n // eye y, eye z, view pitch (radians), projection focal (1 / tan(fov / 2))\n camera : vec4f,\n}\n\n@group(0) @binding(1) var<uniform> gu : GraphUniforms;\n@group(0) @binding(4) var t_field : texture_2d<f32>;\n@group(0) @binding(5) var t_normal : texture_2d<f32>;\n\n// --- the pointer, in the world --------------------------------------------\n// The camera is fixed, so the pointer can be unprojected analytically: undo\n// the projection's focal scaling, undo the view's pitch, and the uv the\n// runtime delivers becomes a ray from the eye into the scene. Variants\n// intersect it with whatever surface they draw on.\n\nstruct PointerRay {\n origin : vec3f,\n dir : vec3f,\n}\n\n// The ray through any point of the frame, uv in 0..1 from the top left. Taking\n// the position rather than reading `params.pointer` lets a variant aim at a\n// place in the frame it picks itself.\nfn st_screenRay(uv : vec2f) -> PointerRay {\n let aspect = gu.viewport.x / max(gu.viewport.y, 1.0);\n let f = gu.camera.w;\n let ndc = vec2f(uv.x * 2.0 - 1.0, (1.0 - uv.y) * 2.0 - 1.0);\n let viewDir = normalize(vec3f(ndc.x * aspect / f, ndc.y / f, -1.0));\n // The view rotates the world by `camera.z` about x; the ray rotates back.\n let c = cos(gu.camera.z);\n let s = sin(gu.camera.z);\n var ray : PointerRay;\n ray.origin = vec3f(0.0, gu.camera.x, gu.camera.y);\n ray.dir = vec3f(viewDir.x, c * viewDir.y + s * viewDir.z, -s * viewDir.y + c * viewDir.z);\n return ray;\n}\n\nfn st_pointerRay() -> PointerRay {\n return st_screenRay(params.pointer);\n}\n\n// Where a point of the frame lands on the y = 0 water plane, in world xz.\nfn st_screenOnPlane(uv : vec2f) -> vec2f {\n let ray = st_screenRay(uv);\n let t = -ray.origin.y / min(ray.dir.y, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return clamp(hit.xz, vec2f(-half), vec2f(half));\n}\n\n// Where the pointer lands on the y = 0 water plane, in world xz.\nfn st_pointerOnPlane() -> vec2f {\n return st_screenOnPlane(params.pointer);\n}\n\n// Where the pointer lands on the vertical plane z = depth, in world xy.\nfn st_pointerOnDepth(depth : f32) -> vec2f {\n let ray = st_pointerRay();\n let t = (depth - ray.origin.z) / min(ray.dir.z, -0.02);\n let hit = ray.origin + ray.dir * t;\n let half = gu.world.x * 0.5;\n return vec2f(clamp(hit.x, -half, half), clamp(hit.y, -20.0, 120.0));\n}\n\n// --- displacement field ------------------------------------------------------\n\n@fragment fn fs_field(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = (uv - vec2f(0.5)) * gu.world.x;\n return st_field(p, st_time());\n}\n\n// --- normals ----------------------------------------------------------------\n\nfn st_loadField(coord : vec2i) -> vec4f {\n let n = i32(gu.viewport.w);\n let c = clamp(coord, vec2i(0), vec2i(n - 1, n - 1));\n return textureLoad(t_field, c, 0);\n}\n\n@fragment fn fs_normals(@builtin(position) position : vec4f) -> @location(0) vec4f {\n let coord = vec2i(position.xy - vec2f(0.5));\n let dx = gu.world.w;\n let s = gu.world.y;\n let r = st_loadField(coord + vec2i(1, 0));\n let l = st_loadField(coord - vec2i(1, 0));\n let t = st_loadField(coord + vec2i(0, 1));\n let b = st_loadField(coord - vec2i(0, 1));\n let dhdx = (r.y - l.y) * s / (2.0 * dx);\n let dhdz = (t.y - b.y) * s / (2.0 * dx);\n let normal = normalize(vec3f(-dhdx, 1.0, -dhdz));\n return vec4f(normal, st_loadField(coord).w);\n}\n\n// --- particles ----------------------------------------------------------------\n\nstruct ParticleOut {\n @builtin(position) position : vec4f,\n @location(0) pointCoord : vec2f,\n @location(1) aux : f32,\n @location(2) normal : vec3f,\n @location(3) viewDir : vec3f,\n @location(4) height : f32,\n @location(5) fade : f32,\n}\n\nfn st_quadCorner(vertexIndex : u32) -> vec2f {\n var corners = array<u32, 6>(0u, 1u, 2u, 2u, 1u, 3u);\n let cornerIndex = corners[vertexIndex % 6u];\n switch (cornerIndex) {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n@vertex fn vs_particles(\n @builtin(vertex_index) vertexIndex : u32,\n @builtin(instance_index) instanceIndex : u32,\n) -> ParticleOut {\n let resolution = max(1u, u32(gu.viewport.w));\n let i = instanceIndex % resolution;\n let j = instanceIndex / resolution;\n let particleRef = vec2f(f32(i), f32(j)) / f32(resolution);\n let texCoord = vec2u(i, j);\n\n let raw = textureLoad(t_field, texCoord, 0);\n let disp = raw.xyz * gu.world.y;\n let nf = textureLoad(t_normal, texCoord, 0);\n\n let halfWorld = gu.world.x * 0.5;\n let base = vec3f(\n particleRef.x * gu.world.x - halfWorld,\n 0.0,\n particleRef.y * gu.world.x - halfWorld,\n );\n let pos = base + disp;\n\n let mv = gu.view * vec4f(pos, 1.0);\n let viewDir = -mv.xyz;\n let dist = -mv.z;\n let f = 1.0 - smoothstep(gu.fade.x, gu.fade.y, dist);\n let fade = pow(clamp(f, 0.0, 1.0), gu.fade.z);\n\n let projected = gu.projection * mv;\n let ndc = projected.xy / projected.w;\n\n let corner = st_quadCorner(vertexIndex);\n let pointSizePx = 2.0 * gu.world.z * gu.viewport.z;\n let clipOffset = corner * (pointSizePx / gu.viewport.xy) * projected.w;\n let clip = vec4f(ndc * projected.w + clipOffset, projected.z, projected.w);\n\n var out : ParticleOut;\n out.position = clip;\n out.pointCoord = corner * 0.5 + vec2f(0.5);\n out.aux = nf.w;\n out.normal = nf.xyz;\n out.viewDir = viewDir;\n out.height = raw.y;\n out.fade = fade;\n return out;\n}\n\n@fragment fn fs_particles(in : ParticleOut) -> @location(0) vec4f {\n let cc = in.pointCoord - vec2f(0.5);\n if (dot(cc, cc) > 0.25) { discard; }\n\n let n = normalize(in.normal);\n let v = normalize(in.viewDir);\n let fresnel = pow(1.0 - clamp(dot(n, v), 0.0, 1.0), 5.0);\n\n let aux = clamp(in.aux, 0.0, 1.0);\n let crest = smoothstep(-0.5, 1.5, in.height);\n\n var color = params.colorA.rgb * 0.5;\n color = color + params.colorB.rgb * crest * 0.5;\n color = color + params.colorB.rgb * fresnel * 0.15;\n color = mix(color, params.colorC.rgb, aux);\n\n // The source example exposed 512² splats; this grid is 256², so each of the\n // quarter as many splats carries four times the weight to match.\n var alpha = (0.02 + crest * 0.06 + fresnel * 0.04) * 4.0;\n alpha = mix(alpha, 1.0, aux);\n color = color * in.fade * params.intensity;\n alpha = clamp(alpha * in.fade, 0.0, 1.0);\n return vec4f(color, alpha);\n}\n\n// ---------------------------------------------------------------------------\n// pulsar\n// ---------------------------------------------------------------------------\n\n// Pulsar — a breathing globe of particles that leans toward your cursor.\n//\n// The grid gives up being a floor entirely: columns become longitude, rows\n// become latitude, and every particle is lifted off the sea plane onto a\n// sphere hanging in the dark. The surface carries a slow relief of 3D noise\n// sampled on the sphere itself — no seams, because the noise never sees the\n// uv wrap — and bands of light sweep the latitudes like a lighthouse turned\n// inside out. The pointer is a gentle gravity: wherever the cursor sits, the\n// whole globe drifts that way and stretches into a soft teardrop, brightest\n// on the side being pulled.\n\n// The centre sits below the frame, so what shows is the upper hemisphere —\n// a planet rising into the picture rather than a ball floating in it.\nconst PULSAR_CENTER : vec3f = vec3f(0.0, -12.0, 38.0);\nconst PULSAR_RADIUS : f32 = 42.0;\n\nfn st_field(p : vec2f, t : f32) -> vec4f {\n // Column → longitude (wrapping), row → latitude.\n let lon = (p.x / gu.world.x + 0.5) * TAU + t * (0.1 + params.d * 0.25);\n let lat = (p.y / gu.world.x + 0.5) * PI;\n let n = vec3f(sin(lat) * cos(lon), cos(lat), sin(lat) * sin(lon));\n\n let radius = PULSAR_RADIUS * params.scale;\n\n // Breath: the whole globe swells and settles.\n var r = radius * (1.0 + 0.05 * params.c * sin(t * 1.3));\n\n // Relief: noise on the sphere, drifting through itself, so the surface\n // boils slowly instead of scrolling.\n let relief = st_fbm3(n * 2.6 + vec3f(0.0, t * 0.1, 0.0), 4);\n r = r + (relief - 0.5) * radius * 0.28 * params.b;\n\n // The attraction: the point on the pointer's ray nearest the globe is\n // where the cursor \"is\" in the globe's space. The whole body drifts that\n // way, capped so it leans rather than leaves, and the surface facing the\n // pull stretches into a teardrop.\n let ray = st_pointerRay();\n let along = max(dot(PULSAR_CENTER - ray.origin, ray.dir), 0.0);\n let cursor = ray.origin + ray.dir * along;\n let delta = cursor - PULSAR_CENTER;\n let dist = length(delta);\n let pullDir = delta / max(dist, 0.001);\n // Full lean once the cursor is a couple of radii away; dead centre is rest.\n let lean = min(dist / (radius * 2.0), 1.0) * params.warp;\n let facing = max(dot(n, pullDir), 0.0);\n r = r + facing * facing * radius * 0.1 * lean;\n let drift = pullDir * radius * 0.12 * lean;\n\n // Bands: light sweeping the latitudes, sharpened to streaks, broken up by\n // the relief so they read as weather rather than as rings.\n let sweep = sin(lat * (3.0 + params.a * 5.0) + t * 1.6 + relief * 3.0);\n let bands = pow(clamp(sweep * 0.5 + 0.5, 0.0, 1.0), 6.0);\n\n // The pulled side glows, so the attraction is readable even mid-lean.\n // Capped low: sixty-five thousand additive splats share a small circle of\n // screen here, and full-strength aux would burn the core to white.\n let aux = clamp(bands * 0.45 + facing * facing * lean * 0.35 + (relief - 0.5) * 0.25, 0.0, 0.6);\n\n let shell = PULSAR_CENTER + drift + n * r;\n let s = gu.world.y;\n return vec4f(shell.x - p.x, shell.y, shell.z - p.y, aux) / vec4f(s, s, s, 1.0);\n}\n";
|
|
52
53
|
readonly radiance: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// radiance\n// ---------------------------------------------------------------------------\n\n// Radiance — light traced from where it lands back to whatever emitted it.\n//\n// Global illumination taken the short way round. There is no light pass and no\n// probe grid to merge: the room is four signed distances, so every pixel can\n// fan rays into it and march each one to its source inside the same fragment\n// that needs the answer. What that buys is a true shadow — hard where a lamp is\n// small, soft where it is near, and cast by geometry that moves every frame.\n\nstruct Surface {\n dist : f32,\n emit : vec3f,\n}\n\nfn radDisc(p : vec2f, centre : vec2f, radius : f32) -> f32 {\n return length(p - centre) - radius;\n}\n\nfn radBar(p : vec2f, centre : vec2f, half : vec2f, angle : f32) -> f32 {\n let q = abs(st_rot(angle) * (p - centre)) - half;\n return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0);\n}\n\nfn radNearer(a : Surface, b : Surface) -> Surface {\n if (b.dist < a.dist) { return b; }\n return a;\n}\n\n// Three drifting lamps, the pointer, and three turning bars that emit nothing.\n// A ray that lands on a bar comes back with black, which is the whole of the\n// shadowing — no shadow term, just an emitter that is not there.\nfn radScene(p : vec2f, t : f32, walls : f32) -> Surface {\n var hit = Surface(1e9, vec3f(0.0));\n\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let angle = t * (0.23 + fi * 0.11) + fi * 2.094;\n let centre = vec2f(cos(angle) * (0.34 - fi * 0.07), sin(angle * 1.4) * (0.24 - fi * 0.05));\n let lamp = Surface(\n radDisc(p, centre, 0.016 + fi * 0.007),\n st_palette(fi * 0.5) * (3.4 - fi * 0.7),\n );\n hit = radNearer(hit, lamp);\n }\n\n // The pointer is the brightest lamp in the room, and it sits in the middle\n // until something moves it — the still frame has to stand on its own.\n let cursor = vec2f((params.pointer.x - 0.5) * st_aspect(), params.pointer.y - 0.5);\n hit = radNearer(hit, Surface(radDisc(p, cursor, 0.028), st_palette(1.0) * 4.2));\n\n if (walls > 0.001) {\n for (var i = 0; i < 3; i = i + 1) {\n let fi = f32(i);\n let orbit = fi * 2.094 - t * 0.17;\n let centre = vec2f(cos(orbit), sin(orbit)) * 0.21;\n let bar = radBar(p, centre, vec2f(0.17, 0.012) * walls, orbit * 1.7 + fi);\n hit = radNearer(hit, Surface(bar, vec3f(0.0)));\n }\n }\n\n return hit;\n}\n\n// Jimenez's interleaved gradient noise. Turning every pixel's fan by the same\n// angle bands the penumbrae into spokes, and the obvious fixes trade that for\n// something worse: white noise clumps, and a low-discrepancy sequence lays the\n// offset down in diagonal stripes. This one was built for exactly this job.\nfn radIgn(pixel : vec2f) -> f32 {\n return fract(52.9829189 * fract(dot(pixel, vec2f(0.06711056, 0.00583715))));\n}\n\n// One ray, marched until it meets something or runs out of room. `start` skips\n// the surface the pixel is already standing on, so a lamp does not shadow\n// itself into a black disc.\nfn radTrace(origin : vec2f, dir : vec2f, start : f32, t : f32, reach : f32, walls : f32) -> vec3f {\n var travel = start;\n for (var i = 0; i < 26; i = i + 1) {\n let hit = radScene(origin + dir * travel, t, walls);\n if (hit.dist < 0.0025) {\n // Inverse square, offset by one so a pixel touching a lamp does not\n // divide by nothing and blow the frame out.\n let fall = travel / max(reach, 0.02);\n return hit.emit / (1.0 + fall * fall);\n }\n travel = travel + max(hit.dist, 0.004);\n if (travel > 3.0) { break; }\n }\n return vec3f(0.0);\n}\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let t = st_time();\n let p = st_centered(uv) / max(params.scale, 0.05);\n\n let rays = i32(clamp(params.a, 3.0, 48.0));\n let walls = params.c;\n\n let cellId = floor(uv * params.resolution);\n let dither = radIgn(cellId);\n let turn = dither * TAU;\n\n let standing = radScene(p, t, walls);\n // Staggering where the rays begin as well as where they point breaks up the\n // ring the shared start distance would otherwise leave around every lamp.\n let start = max(-standing.dist, 0.0) + 0.004 + dither * 0.006;\n\n var sum = vec3f(0.0);\n for (var i = 0; i < rays; i = i + 1) {\n // A third of a wedge of slop on top of the ordered offset. Full white\n // noise per ray throws the stratification away and the frame goes\n // sandy; this much only breaks the pattern's grip on the lattice.\n let slop = (st_hash21(cellId + f32(i) * 17.31) - 0.5) * 0.35;\n let angle = turn + TAU * (f32(i) + 0.5 + slop) / f32(rays);\n sum = sum + radTrace(p, vec2f(cos(angle), sin(angle)), start, t, params.b, walls);\n }\n\n var col = sum / f32(rays) * params.intensity;\n\n // A lamp lights its own pixels. Without this the rays all leave from its\n // surface facing outwards, and the brightest thing in the room is a hole.\n col = col + standing.emit * (1.0 - smoothstep(-0.002, 0.002, standing.dist));\n\n // Skylight: the floor of the image, so the bars read as objects in a room\n // rather than as holes cut out of it.\n col = col + params.colorA.rgb * params.d;\n\n return st_post(st_tonemap(col), uv);\n}\n";
|
|
53
54
|
readonly reveal: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// shadertown source prelude — appended to the shared prelude for shaders that\n// read a layer instead of inventing one.\n//\n// Every binding here is additive. A generative shader declares none of them and\n// keeps the exact layout it always had, and `Source` is its own uniform block\n// rather than four more fields on `Params`, so the ABI that every already\n// shipped shader was built against never moves.\n\n@group(0) @binding(1) var srcTexture : texture_2d<f32>;\n@group(0) @binding(2) var srcSampler : sampler;\n\nstruct Source {\n // Pixel size of the bound texture. Zero means \"assume it matches the canvas\".\n size : vec2f,\n // 0 stretches the source over the canvas, 1 covers the canvas and crops.\n fit : f32,\n _pad : f32,\n}\n\n@group(0) @binding(3) var<uniform> source : Source;\n\n// How much of the source each axis shows once it is fitted. Both components are\n// 1 or less: covering means cropping, never adding.\nfn st_srcScale() -> vec2f {\n if (source.fit < 0.5 || source.size.x < 1.0 || source.size.y < 1.0) {\n return vec2f(1.0);\n }\n let canvas = st_aspect();\n let src = source.size.x / source.size.y;\n if (src > canvas) {\n return vec2f(canvas / src, 1.0);\n }\n return vec2f(1.0, src / canvas);\n}\n\n/** Canvas uv to source uv. */\nfn st_srcUv(uv : vec2f) -> vec2f {\n return (uv - 0.5) * st_srcScale() + 0.5;\n}\n\n/**\n * Canvas uv to source uv, with room left around the edge.\n *\n * A shader that displaces its reads runs off the source at the border, and the\n * sampler answers by smearing its last row of texels across the gap. Reading a\n * slightly smaller crop gives the displacement somewhere to go, at the cost of\n * a few per cent of the picture.\n */\nfn st_srcUvInset(uv : vec2f, margin : f32) -> vec2f {\n return (uv - 0.5) * st_srcScale() * (1.0 - clamp(margin, 0.0, 0.4)) + 0.5;\n}\n\n/**\n * A canvas-space displacement, in source uv.\n *\n * Divides out the aspect so a push of 0.01 moves the same distance sideways as\n * it does up, whatever shape the canvas or the source is.\n */\nfn st_srcWarp(offset : vec2f) -> vec2f {\n return offset * st_srcScale() / vec2f(st_aspect(), 1.0);\n}\n\n/** Samples in source uv. Kernels that walk neighbours want this one. */\nfn st_srcAt(srcUv : vec2f) -> vec4f {\n return textureSample(srcTexture, srcSampler, srcUv);\n}\n\n/** Samples in canvas uv — the ordinary read. */\nfn st_src(uv : vec2f) -> vec4f {\n return st_srcAt(st_srcUv(uv));\n}\n\nfn st_srcRgb(uv : vec2f) -> vec3f {\n return st_src(uv).rgb;\n}\n\n/** One source texel, in source uv. */\nfn st_srcTexel() -> vec2f {\n return 1.0 / max(source.size, vec2f(1.0));\n}\n\n/**\n * 1 inside the source, falling to 0 just outside it.\n *\n * The sampler clamps, so a shader that pushes past the border smears the last\n * row of texels across the gap. Anything that displaces should fade instead.\n */\nfn st_srcInside(srcUv : vec2f) -> f32 {\n let edge = min(srcUv, vec2f(1.0) - srcUv);\n return smoothstep(0.0, 0.004, min(edge.x, edge.y));\n}\n\n/** Four-tap softening. Cheap enough to run per colour channel. */\nfn st_srcSoften(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = vec3f(0.0);\n for (var i = 0; i < 4; i = i + 1) {\n let angle = f32(i) * 1.570796 + 0.785398;\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * radius)).rgb;\n }\n return sum * 0.25;\n}\n\n/**\n * Thirteen-tap spiral blur. `radius` is in canvas space.\n *\n * The golden angle keeps the taps from lining up into spokes, which is what a\n * uniform ring blur gives you at low tap counts.\n */\nfn st_srcBlur(uv : vec2f, radius : f32) -> vec3f {\n let base = st_srcUv(uv);\n var sum = st_srcAt(base).rgb;\n for (var i = 0; i < 12; i = i + 1) {\n let fi = f32(i);\n let angle = fi * 2.399963;\n let r = radius * sqrt((fi + 0.5) / 12.0);\n sum = sum + st_srcAt(base + st_srcWarp(vec2f(cos(angle), sin(angle)) * r)).rgb;\n }\n return sum / 13.0;\n}\n\n// ---------------------------------------------------------------------------\n// reveal\n// ---------------------------------------------------------------------------\n\n// Reveal — the layer wiped in over a colour ground.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n // At speed 0, progress is exactly the control: a scrubber to drive from your\n // own code, bound to a scroll position or a load event.\n //\n // Running, it wipes in, holds the layer for most of the cycle, then clears.\n // A linear loop would spend half its time on an empty ground, and the offset\n // starts it revealed so the first frame is the picture and not the backdrop.\n let phase = fract(params.a + 0.4 + st_time() * 0.25);\n let cycled = smoothstep(0.0, 0.3, phase) * (1.0 - smoothstep(0.9, 1.0, phase));\n let progress = select(cycled, params.a, params.speed < 0.001);\n\n // The field the threshold cuts through. Noise dissolves, a rotated ramp\n // wipes, and `shape` slides between the two.\n let noise = st_fbm2(st_centered(uv) * params.scale * 4.0, 3);\n let axis = st_rot(params.c * TAU) * (uv - 0.5);\n let ramp = clamp(axis.x + 0.5, 0.0, 1.0);\n let field = mix(noise, ramp, clamp(params.b, 0.0, 1.0));\n\n // Widen the threshold band to soften the boundary. It has to cover the\n // whole 0..1 range at both ends, or the wipe never finishes.\n let soft = max(params.d, 0.001);\n let cut = progress * (1.0 + 2.0 * soft) - soft;\n let mask = 1.0 - smoothstep(cut - soft, cut + soft, field);\n\n // The ground follows the clean ramp, never the noise. Colouring it by the\n // dissolve field would leave static behind the wipe instead of a backdrop.\n let ground = st_palette(ramp);\n var col = mix(ground, st_srcRgb(uv), mask);\n\n // A bright rim rides the boundary, the way a burn does.\n let rim = 1.0 - abs(mask - 0.5) * 2.0;\n col = col + st_palette(1.0) * pow(rim, 3.0) * params.warp;\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
54
55
|
readonly ripple: "// shadertown shared prelude — prepended to every shader at build time.\n// One uniform block is the whole ABI: every shader reads the same struct,\n// so the runtime can swap shaders without changing a binding.\n\nstruct Params {\n resolution : vec2f,\n pointer : vec2f,\n time : f32,\n speed : f32,\n scale : f32,\n intensity : f32,\n vibrancy : f32,\n grain : f32,\n vignette : f32,\n warp : f32,\n a : f32,\n b : f32,\n c : f32,\n d : f32,\n colorA : vec4f,\n colorB : vec4f,\n colorC : vec4f,\n}\n\n@group(0) @binding(0) var<uniform> params : Params;\n\nconst PI : f32 = 3.141592653589793;\nconst TAU : f32 = 6.283185307179586;\n\nfn st_time() -> f32 {\n return params.time * params.speed;\n}\n\n// Centred, aspect-corrected coordinates. y stays in [-0.5, 0.5].\nfn st_centered(uv : vec2f) -> vec2f {\n let aspect = params.resolution.x / max(params.resolution.y, 1.0);\n return vec2f((uv.x - 0.5) * aspect, uv.y - 0.5);\n}\n\nfn st_aspect() -> f32 {\n return params.resolution.x / max(params.resolution.y, 1.0);\n}\n\nfn st_rot(angle : f32) -> mat2x2f {\n let s = sin(angle);\n let c = cos(angle);\n return mat2x2f(c, -s, s, c);\n}\n\n// The device-pixel size of one virtual cell, for pixel-art shaders. Cells\n// must be a whole number of device pixels: dividing the canvas by the\n// requested count directly leaves some cells one pixel fatter than others,\n// which the eye reads instantly as a broken grid. Use as:\n// let px = st_pixelSize(params.a);\n// let cell = floor(uv * params.resolution / px);\n// let puv = (cell + 0.5) * px / params.resolution;\nfn st_pixelSize(pixels : f32) -> f32 {\n return max(round(params.resolution.y / max(pixels, 8.0)), 1.0);\n}\n\n// --- hashing ---------------------------------------------------------------\n\nfn st_hash11(x : f32) -> f32 {\n var p = fract(x * 0.1031);\n p = p * (p + 33.33);\n return fract(p * (p + p));\n}\n\nfn st_hash21(p : vec2f) -> f32 {\n var p3 = fract(vec3f(p.xyx) * 0.1031);\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\n\nfn st_hash22(p : vec2f) -> vec2f {\n var p3 = fract(vec3f(p.xyx) * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yzx + 33.33);\n return fract((p3.xx + p3.yz) * p3.zy);\n}\n\nfn st_hash33(p : vec3f) -> vec3f {\n var p3 = fract(p * vec3f(0.1031, 0.1030, 0.0973));\n p3 = p3 + dot(p3, p3.yxz + 33.33);\n return fract((p3.xxy + p3.yxx) * p3.zyx);\n}\n\n// --- noise -----------------------------------------------------------------\n\nfn st_noise2(p : vec2f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let a = st_hash21(i);\n let b = st_hash21(i + vec2f(1.0, 0.0));\n let c = st_hash21(i + vec2f(0.0, 1.0));\n let d = st_hash21(i + vec2f(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfn st_noise3(p : vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let n000 = st_hash33(i + vec3f(0.0, 0.0, 0.0)).x;\n let n100 = st_hash33(i + vec3f(1.0, 0.0, 0.0)).x;\n let n010 = st_hash33(i + vec3f(0.0, 1.0, 0.0)).x;\n let n110 = st_hash33(i + vec3f(1.0, 1.0, 0.0)).x;\n let n001 = st_hash33(i + vec3f(0.0, 0.0, 1.0)).x;\n let n101 = st_hash33(i + vec3f(1.0, 0.0, 1.0)).x;\n let n011 = st_hash33(i + vec3f(0.0, 1.0, 1.0)).x;\n let n111 = st_hash33(i + vec3f(1.0, 1.0, 1.0)).x;\n let x00 = mix(n000, n100, u.x);\n let x10 = mix(n010, n110, u.x);\n let x01 = mix(n001, n101, u.x);\n let x11 = mix(n011, n111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\nfn st_fbm2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.5);\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise2(shifted);\n shifted = rotation * shifted * 2.02;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\nfn st_fbm3(p : vec3f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n for (var i = 0; i < octaves; i = i + 1) {\n value = value + amplitude * st_noise3(shifted);\n shifted = shifted * 2.03;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// Ridged noise — sharp crests instead of soft hills.\nfn st_ridge2(p : vec2f, octaves : i32) -> f32 {\n var value = 0.0;\n var amplitude = 0.5;\n var shifted = p;\n let rotation = st_rot(0.7);\n for (var i = 0; i < octaves; i = i + 1) {\n let n = 1.0 - abs(st_noise2(shifted) * 2.0 - 1.0);\n value = value + amplitude * n * n;\n shifted = rotation * shifted * 2.11;\n amplitude = amplitude * 0.5;\n }\n return value;\n}\n\n// --- colour ----------------------------------------------------------------\n\n// Three-stop ramp built from the user's colours. t is clamped to [0, 1].\nfn st_palette(t : f32) -> vec3f {\n let x = clamp(t, 0.0, 1.0);\n let low = mix(params.colorA.rgb, params.colorB.rgb, smoothstep(0.0, 0.5, x));\n return mix(low, params.colorC.rgb, smoothstep(0.5, 1.0, x));\n}\n\n// Same ramp, but wrapping — good for cycling and polar shaders.\nfn st_paletteCyclic(t : f32) -> vec3f {\n let x = fract(t);\n let tri = 1.0 - abs(x * 2.0 - 1.0);\n return st_palette(tri);\n}\n\nfn st_luma(c : vec3f) -> f32 {\n return dot(c, vec3f(0.2126, 0.7152, 0.0722));\n}\n\nfn st_saturation(c : vec3f, amount : f32) -> vec3f {\n return mix(vec3f(st_luma(c)), c, amount);\n}\n\nfn st_tonemap(c : vec3f) -> vec3f {\n let x = max(c, vec3f(0.0));\n return (x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14);\n}\n\n// --- shared post chain -----------------------------------------------------\n// Every shader ends here, so vibrancy / grain / vignette behave identically\n// across the whole catalogue.\nfn st_post(color : vec3f, uv : vec2f) -> vec4f {\n var c = st_saturation(max(color, vec3f(0.0)), params.vibrancy);\n\n if (params.grain > 0.0) {\n let seed = uv * params.resolution + vec2f(params.time * 61.7, params.time * 37.3);\n let n = st_hash21(floor(seed));\n c = c + (n - 0.5) * params.grain;\n }\n\n if (params.vignette > 0.0) {\n let d = length(uv - vec2f(0.5)) * 1.41421356;\n c = c * (1.0 - params.vignette * smoothstep(0.22, 1.0, d));\n }\n\n return vec4f(clamp(c, vec3f(0.0), vec3f(1.0)), 1.0);\n}\n\n// ---------------------------------------------------------------------------\n// ripple\n// ---------------------------------------------------------------------------\n\n// Ripple — interference between travelling circular waves.\n\n@fragment fn fs_main(@location(0) uv : vec2f) -> @location(0) vec4f {\n let p = st_centered(uv) * params.scale;\n let t = st_time();\n\n let sourceCount = 5;\n var wave = 0.0;\n var gradient = vec2f(0.0);\n\n for (var i = 0; i < sourceCount; i = i + 1) {\n let fi = f32(i);\n let phase = fi * 2.399963;\n let orbit = 0.28 * params.b;\n let src = vec2f(cos(t * 0.23 + phase) * orbit, sin(t * 0.31 + phase * 1.7) * orbit);\n let delta = p - src;\n let d = length(delta) + 0.0001;\n let k = 40.0 * params.a;\n let decay = 1.0 / (1.0 + d * 6.0);\n let s = sin(d * k - t * 4.0 + phase) * decay;\n wave = wave + s;\n gradient = gradient + (delta / d) * cos(d * k - t * 4.0 + phase) * decay * k;\n }\n\n wave = wave / f32(sourceCount);\n gradient = gradient / f32(sourceCount);\n\n // Treat the wave slope as a refracting surface and read the palette through it.\n let refracted = clamp(0.5 + wave * 1.4 + gradient.x * 0.02 * params.c, 0.0, 1.0);\n var col = st_palette(refracted);\n\n // Caustic highlights where the wavefronts focus.\n let caustic = pow(clamp(1.0 - abs(wave) * 3.2, 0.0, 1.0), 6.0);\n col = col + vec3f(1.0) * caustic * params.d;\n\n return st_post(col * params.intensity, uv);\n}\n";
|
|
@@ -73,6 +74,7 @@ export declare const shaderKinds: {
|
|
|
73
74
|
readonly beacon: "generative";
|
|
74
75
|
readonly bloom: "source";
|
|
75
76
|
readonly blossom: "generative";
|
|
77
|
+
readonly blueprint: "graph";
|
|
76
78
|
readonly bokeh: "generative";
|
|
77
79
|
readonly caldera: "generative";
|
|
78
80
|
readonly caustics: "generative";
|
|
@@ -133,5 +135,87 @@ export declare const shaderKinds: {
|
|
|
133
135
|
readonly tunnel: "generative";
|
|
134
136
|
readonly voronoi: "generative";
|
|
135
137
|
};
|
|
138
|
+
/**
|
|
139
|
+
* Whether a shader binds a source layer: every source-kind shader, and the
|
|
140
|
+
* graph shaders whose flavour prelude asked for one.
|
|
141
|
+
*/
|
|
142
|
+
export declare const shaderReadsSource: {
|
|
143
|
+
readonly archipelago: false;
|
|
144
|
+
readonly ascii: true;
|
|
145
|
+
readonly aurora: false;
|
|
146
|
+
readonly beacon: false;
|
|
147
|
+
readonly bloom: true;
|
|
148
|
+
readonly blossom: false;
|
|
149
|
+
readonly blueprint: true;
|
|
150
|
+
readonly bokeh: false;
|
|
151
|
+
readonly caldera: false;
|
|
152
|
+
readonly caustics: false;
|
|
153
|
+
readonly chladni: false;
|
|
154
|
+
readonly cloth: false;
|
|
155
|
+
readonly cumulus: false;
|
|
156
|
+
readonly dither: false;
|
|
157
|
+
readonly dunes: false;
|
|
158
|
+
readonly duotone: true;
|
|
159
|
+
readonly edges: true;
|
|
160
|
+
readonly eyes: false;
|
|
161
|
+
readonly ferro: false;
|
|
162
|
+
readonly "film-grain": false;
|
|
163
|
+
readonly flow: true;
|
|
164
|
+
readonly fluid: false;
|
|
165
|
+
readonly glass: true;
|
|
166
|
+
readonly gravity: false;
|
|
167
|
+
readonly halftone: false;
|
|
168
|
+
readonly hills: false;
|
|
169
|
+
readonly island: false;
|
|
170
|
+
readonly julia: false;
|
|
171
|
+
readonly kaleidoscope: false;
|
|
172
|
+
readonly lagoon: false;
|
|
173
|
+
readonly lens: true;
|
|
174
|
+
readonly "liquid-metal": false;
|
|
175
|
+
readonly lumen: false;
|
|
176
|
+
readonly maelstrom: false;
|
|
177
|
+
readonly magnet: false;
|
|
178
|
+
readonly meadow: false;
|
|
179
|
+
readonly melt: true;
|
|
180
|
+
readonly "mesh-gradient": false;
|
|
181
|
+
readonly mosaic: true;
|
|
182
|
+
readonly "neon-grid": false;
|
|
183
|
+
readonly "neon-sign": true;
|
|
184
|
+
readonly orb: false;
|
|
185
|
+
readonly parallax: false;
|
|
186
|
+
readonly particles: false;
|
|
187
|
+
readonly "pixel-clouds": false;
|
|
188
|
+
readonly "pixel-fire": false;
|
|
189
|
+
readonly "pixel-isle": false;
|
|
190
|
+
readonly "pixel-rain": false;
|
|
191
|
+
readonly "pixel-sea": false;
|
|
192
|
+
readonly plasma: false;
|
|
193
|
+
readonly pulsar: false;
|
|
194
|
+
readonly radiance: false;
|
|
195
|
+
readonly reveal: true;
|
|
196
|
+
readonly ripple: false;
|
|
197
|
+
readonly shatter: true;
|
|
198
|
+
readonly shockwave: true;
|
|
199
|
+
readonly shore: false;
|
|
200
|
+
readonly silk: false;
|
|
201
|
+
readonly starfield: false;
|
|
202
|
+
readonly swell: false;
|
|
203
|
+
readonly swirl: true;
|
|
204
|
+
readonly tesla: false;
|
|
205
|
+
readonly torch: false;
|
|
206
|
+
readonly truchet: false;
|
|
207
|
+
readonly tunnel: false;
|
|
208
|
+
readonly voronoi: false;
|
|
209
|
+
};
|
|
210
|
+
/** Which render graph drives a graph shader; absent for the other kinds. */
|
|
211
|
+
export declare const shaderGraphs: {
|
|
212
|
+
readonly blueprint: "drafting";
|
|
213
|
+
readonly chladni: "particles";
|
|
214
|
+
readonly dunes: "particles";
|
|
215
|
+
readonly ferro: "particles";
|
|
216
|
+
readonly maelstrom: "particles";
|
|
217
|
+
readonly pulsar: "particles";
|
|
218
|
+
};
|
|
136
219
|
export type GeneratedShaderName = keyof typeof shaderSources;
|
|
220
|
+
export type GraphFlavour = (typeof shaderGraphs)[keyof typeof shaderGraphs];
|
|
137
221
|
//# sourceMappingURL=sources.d.ts.map
|