Examples
generic metaballs.ts
Metaballs
Implicit blobs — each ball adds an inverse-square field, the fields sum, and the iso-contour merges and splits as they orbit. Move the pointer and one ball follows it into the swarm. Ball count is live.
Live
Pointer-interactive
0.0s
One source, compiled every way
The DSL source below, compiled to WebGPU (WGSL), WebGL2 (GLSL ES 3.00 — two stages), and a reflection JSON the host binds from. Nothing but the DSL tab is hand-written.
// ═══ @xgis/shader-dsl example — metaballs (implicit blob field) ═══//// The classic 2D metaballs: each ball contributes an inverse-square field// w = k/d², the fields SUM, and the iso-contour of the summed field is the// blobby silhouette that merges and splits as the balls orbit. Colour is the// field-weighted blob index through a cosine palette, so blobs keep their hue// while blending where they touch. Showcases a Loop accumulator over a// uniform-driven ball count. WGSL (WebGPU) + GLSL ES 3.00 (WebGL2).
import { fn, module, u32, f32, toF32, vec2, vec3, vec4, sin, cos, dot, max, mix, smoothstep, Loop, Var, Let, f32T, vec4fT,} from '../src/index.ts'import { VsOut, vs, fullscreenUniforms, screenCoords } from './_fullscreen.ts'import type { ShaderExample } from './_shared.ts'const U = fullscreenUniforms({ count: f32T, mouse: vec4fT })
const palette = fn('palette', { t: f32T }, ({ t }) => { const ph = vec3(0.0, 0.33, 0.67) return vec3(0.5).add(cos(t.add(ph).mul(6.283)).mul(0.5))})
const fs = fn( 'fs', { vo: VsOut }, ({ vo }) => { const t = U.field.time const res = U.field.resolution const p = screenCoords(vo.uv, res) // once the pointer has entered (m.w = 1), ball 0 leaves its orbit and // follows the cursor — mapped to the same isotropic space as p const m = U.field.mouse const cursor = Let(screenCoords(vec2(m.x.div(res.x), m.y.div(res.y)), res)) // sum the inverse-square fields; hue accumulates field-weighted so merged // blobs blend their colours in proportion to who dominates the pixel const field = Var(f32(0)) const hue = Var(f32(0)) Loop( u32(0), (i) => toF32(i).lt(U.field.count), (i) => { const fi = toF32(i) const orbit = vec2( sin(t.mul(fi.mul(0.13).add(0.5)).add(fi.mul(2.4))).mul(0.55), cos(t.mul(fi.mul(0.11).add(0.4)).add(fi.mul(1.7))).mul(0.42), ) // ball 0 → cursor when interacted: step(fi, 0.5) selects i == 0 const follow = f32(1) .sub(smoothstep(0.4, 0.6, fi)) .mul(m.w) const ctr = mix(orbit, cursor, follow) const d = Let(p.sub(ctr)) const w = Let(f32(0.055).div(dot(d, d).add(0.003))) field.assign(field.add(w)) hue.assign(hue.add(w.mul(fi))) }, ) // iso-contour of the summed field = the blob silhouette const iso = smoothstep(0.95, 1.15, field) const blob = palette({ t: hue.div(max(field, 0.0001)).mul(0.15).add(t.mul(0.03)) }) // outside: dark bg lit by a faint field glow; inside: the full blob colour const bg = vec3(0.03, 0.04, 0.08).add(blob.mul(field.mul(0.08))) return vec4(mix(bg, blob, iso), 1) }, { stage: 'fragment', retAttr: '@location(0)' },)
const metaballsModule = module({ structs: [U.struct, VsOut.decl], bindings: [U.binding], funcs: [vs, fs],})
export const metaballs: ShaderExample = { id: 'metaballs', title: 'Metaballs', blurb: 'Implicit blobs — each ball adds an inverse-square field, the fields sum, and the iso-contour merges and splits as they orbit. Move the pointer and one ball follows it into the swarm. Ball count is live.', category: 'generic', file: 'metaballs.ts', module: metaballsModule, renderable: true, controls: { time: { kind: 'time' }, resolution: { kind: 'resolution' }, count: { kind: 'slider', label: 'Balls', min: 2, max: 6, step: 1, value: 5 }, mouse: { kind: 'mouse' }, },}struct Uniforms { time: f32, resolution: vec2<f32>, count: f32, mouse: vec4<f32>,}
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32>,}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn palette(t: f32) -> vec3<f32> { return (vec3<f32>(0.5) + (cos(((t + vec3<f32>(0.0, 0.33, 0.67)) * 6.283)) * 0.5));}
@vertexfn vs(@builtin(vertex_index) vi: u32) -> VsOut { let _cse0 = ((f32((vi & 1u)) * 4.0) - 1.0); let _cse1 = ((f32((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4<f32>(_cse0, _cse1, 0.0, 1.0), vec2<f32>(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
@fragmentfn fs(vo: VsOut) -> @location(0) vec4<f32> { let _licm0 = U.count; let _licm1 = ((vo.uv.x * 2.0) - 1.0); let _licm2 = ((vo.uv.y * 2.0) - 1.0); let _licm3 = U.time; let _licm4 = U.mouse.w; let _cse0 = vec2<f32>((U.mouse.x / U.resolution.x), (U.mouse.y / U.resolution.y)); let _cse1 = (U.resolution.x / U.resolution.y); let _cse2 = (_licm3 * 0.03); let _v0 = vec2<f32>((((_cse0.x * 2.0) - 1.0) * _cse1), ((_cse0.y * 2.0) - 1.0)); var _v1: f32 = 0.0; var _v2: f32 = 0.0; for (var _v3: u32 = 0u; (f32(_v3) < _licm0); _v3 = (_v3 + 1u)) { let _lc0 = f32(_v3); let _v4 = (vec2<f32>((_licm1 * _cse1), _licm2) - mix(vec2<f32>((sin(((_licm3 * ((_lc0 * 0.13) + 0.5)) + (_lc0 * 2.4))) * 0.55), (cos(((_licm3 * ((_lc0 * 0.11) + 0.4)) + (_lc0 * 1.7))) * 0.42)), _v0, ((1.0 - smoothstep(0.4, 0.6, _lc0)) * _licm4))); let _v5 = (0.055 / (dot(_v4, _v4) + 0.003)); _v1 = (_v1 + _v5); _v2 = (_v2 + (_v5 * f32(_v3))); } let _lc1 = palette((((_v2 / max(_v1, 0.0001)) * 0.15) + _cse2)); return vec4<f32>(mix((vec3<f32>(0.03, 0.04, 0.08) + (_lc1 * (_v1 * 0.08))), _lc1, smoothstep(0.95, 1.15, _v1)), 1.0);}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};out vec2 uv;
VsOut vs_impl(uint vi) { float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0); float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0); return VsOut(vec4(_cse0, _cse1, 0.0, 1.0), vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5)));}
void main() { VsOut _out = vs_impl(uint(gl_VertexID)); gl_Position = _out.pos; uv = _out.uv;}#version 300 esprecision highp float;precision highp int;
struct VsOut { vec4 pos; vec2 uv;};layout(std140) uniform Uniforms { float time; vec2 resolution; float count; vec4 mouse;} U;vec3 palette(float t);vec3 palette(float t) { return (vec3(0.5) + (cos(((t + vec3(0.0, 0.33, 0.67)) * 6.283)) * 0.5));}in vec2 uv;layout(location = 0) out vec4 _ret;
vec4 fs_impl(VsOut vo) { float _licm0 = U.count; float _licm1 = ((vo.uv.x * 2.0) - 1.0); float _licm2 = ((vo.uv.y * 2.0) - 1.0); float _licm3 = U.time; float _licm4 = U.mouse.w; vec2 _cse0 = vec2((U.mouse.x / U.resolution.x), (U.mouse.y / U.resolution.y)); float _cse1 = (U.resolution.x / U.resolution.y); float _cse2 = (_licm3 * 0.03); vec2 _v0 = vec2((((_cse0.x * 2.0) - 1.0) * _cse1), ((_cse0.y * 2.0) - 1.0)); float _v1 = 0.0; float _v2 = 0.0; for (uint _v3 = 0u; (float(_v3) < _licm0); _v3 = (_v3 + 1u)) { float _lc0 = float(_v3); vec2 _v4 = (vec2((_licm1 * _cse1), _licm2) - mix(vec2((sin(((_licm3 * ((_lc0 * 0.13) + 0.5)) + (_lc0 * 2.4))) * 0.55), (cos(((_licm3 * ((_lc0 * 0.11) + 0.4)) + (_lc0 * 1.7))) * 0.42)), _v0, ((1.0 - smoothstep(0.4, 0.6, _lc0)) * _licm4))); float _v5 = (0.055 / (dot(_v4, _v4) + 0.003)); _v1 = (_v1 + _v5); _v2 = (_v2 + (_v5 * float(_v3))); } vec3 _lc1 = palette((((_v2 / max(_v1, 0.0001)) * 0.15) + _cse2)); return vec4(mix((vec3(0.03, 0.04, 0.08) + (_lc1 * (_v1 * 0.08))), _lc1, smoothstep(0.95, 1.15, _v1)), 1.0);}
void main() { VsOut vo; vo.pos = gl_FragCoord; vo.uv = uv; _ret = fs_impl(vo);}{ "bindGroups": [ { "group": 0, "entries": [ { "group": 0, "binding": 0, "name": "U", "space": "uniform", "resourceKind": "uniform-buffer", "structName": "Uniforms" } ] } ], "uniforms": [ { "name": "Uniforms", "size": 48, "align": 16, "fields": [ { "name": "time", "type": "f32", "offset": 0, "align": 4, "size": 4 }, { "name": "resolution", "type": "vec2<f32>", "offset": 8, "align": 8, "size": 8 }, { "name": "count", "type": "f32", "offset": 16, "align": 4, "size": 4 }, { "name": "mouse", "type": "vec4<f32>", "offset": 32, "align": 16, "size": 16 } ] } ], "storage": [], "entries": [ { "name": "vs", "stage": "vertex", "inputs": [ "u32" ], "output": "struct:VsOut" }, { "name": "fs", "stage": "fragment", "inputs": [ "struct:VsOut" ], "output": "vec4<f32>" } ], "overrides": []}
Run it locally:
npx tsx examples/print.ts metaballs
— prints the WGSL, GLSL, reflection. Source:
GitHub.
New to the IR? See Concepts; the full authoring + emit surface is in the API reference.