Voronoi

Voronoi

Animated cellular noise.

Voronoi shader, written in TypeScript and compiled by TypeShade, rendered at build time.
// ═══ typeshade example — animated Voronoi (cellular noise) ═══
//
// The classic ShaderToy cellular pattern: tile the plane into cells, scatter one
// animated feature point per cell, and shade each fragment by the DISTANCE to its
// nearest point (a 3×3 neighbour scan). Showcases the `distance` builtin + the DSL's
// nested `Loop`. One source → WGSL (WebGPU) + GLSL ES 3.00 (WebGL2) + Reflection.
import {
fn,
module,
i32,
f32,
toF32,
vec2,
vec3,
vec4,
sin,
cos,
floor,
fract,
dot,
min,
distance,
Loop,
Let,
f32T,
vec2fT,
} from '../src/index.js';
import { VsOut, vs, fullscreenUniforms } from './_fullscreen.js';
import type { ShaderExample } from './_shared.js';
const U = fullscreenUniforms({ cells: f32T });
// Hash a cell coordinate → a stable point in [0,1]² (the per-cell feature seed).
const hash2 = fn('hash2', { c: vec2fT }, ({ c }) => {
const h = vec2(dot(c, vec2(127.1, 311.7)), dot(c, vec2(269.5, 183.3)));
return fract(vec2(sin(h.x), sin(h.y)).mul(43758.5453));
});
const fs = fn(
'fs',
{ vo: VsOut },
({ vo }) => {
const uv = vo.uv;
const p = uv.mul(U.field.cells);
const cell = floor(p);
const f = fract(p);
const md = f32(8); // nearest-point distance accumulator
// 3×3 neighbour scan — the nearest feature point may live in an adjacent cell.
Loop(
i32(-1),
(j) => j.le(1),
(j) => {
Loop(
i32(-1),
(i) => i.le(1),
(i) => {
const g = Let(vec2(toF32(i), toF32(j)));
const seed = Let(hash2({ c: cell.add(g) })); // materialise once: the loop counters are mutated `var`s, so CSE can't hoist the hash — without Let the 3 `seed.*` reads re-call hash2() per iteration
// animate each point on a small orbit around its cell so the pattern shimmers
const pt = g
.add(seed.mul(0.5).add(0.25))
.add(
vec2(
sin(U.field.time.add(seed.x.mul(6.283))),
cos(U.field.time.add(seed.y.mul(6.283))),
).mul(0.18),
);
md.assign(min(md, distance(f, pt)));
},
);
},
);
// tint the distance field: dark cores, cool cell walls
const c = vec3(md.mul(md))
.mul(vec3(0.35, 0.6, 1.0))
.add(vec3(0.02, 0.03, 0.06));
return vec4(c, 1);
},
{ stage: 'fragment', retAttr: '@location(0)' },
);
// `hash2` is called via its handle in `fs`, so module() collects it transitively — funcs lists only the entry points.
const voronoiModule = module({
structs: [U.struct, VsOut.decl],
bindings: [U.binding],
funcs: [vs, fs],
});
export const voronoi: ShaderExample = {
id: 'voronoi',
title: 'Voronoi',
blurb:
'Animated cellular noise — each fragment shaded by the distance to its nearest of a grid of orbiting feature points (a 3×3 neighbour scan). Built on the `distance` builtin + nested loops.',
category: 'generic',
file: 'voronoi.ts',
module: voronoiModule,
renderable: true,
controls: {
time: { kind: 'time' },
resolution: { kind: 'resolution' },
cells: { kind: 'slider', label: 'Cell density', min: 2, max: 16, step: 1, value: 6 },
},
};
struct Uniforms {
time: f32,
resolution: vec2<f32>,
cells: f32,
}
struct VsOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> U: Uniforms;
fn hash2(c: vec2<f32>) -> vec2<f32> {
let _cse0 = vec2<f32>(dot(c, vec2<f32>(127.1, 311.7)), dot(c, vec2<f32>(269.5, 183.3)));
return fract((vec2<f32>(sin(_cse0.x), sin(_cse0.y)) * 43758.5453));
}
@vertex
fn 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)));
}
@fragment
fn fs(vo: VsOut) -> @location(0) vec4<f32> {
let _licm0 = U.time;
var _av0: f32 = 8.0;
let _cse0 = (vo.uv * U.cells);
for (var _v0: i32 = -1; (_v0 <= 1); _v0 = (_v0 + 1)) {
for (var _v1: i32 = -1; (_v1 <= 1); _v1 = (_v1 + 1)) {
let _v2 = vec2<f32>(f32(_v1), f32(_v0));
let _v3 = hash2((floor(_cse0) + _v2));
_av0 = min(_av0, distance(fract(_cse0), ((_v2 + ((_v3 * 0.5) + 0.25)) + (vec2<f32>(sin((_licm0 + (_v3.x * 6.283))), cos((_licm0 + (_v3.y * 6.283)))) * 0.18))));
}
}
return vec4<f32>(((vec3<f32>((_av0 * _av0)) * vec3<f32>(0.35, 0.6, 1.0)) + vec3<f32>(0.02, 0.03, 0.06)), 1.0);
}
#version 300 es
precision highp float;
precision highp int;
out vec2 uv;
void main() {
uint vi = uint(gl_VertexID);
float _cse0 = ((float((vi & 1u)) * 4.0) - 1.0);
float _cse1 = ((float((vi >> 1u)) * 4.0) - 1.0);
gl_Position = vec4(_cse0, _cse1, 0.0, 1.0);
uv = vec2(((_cse0 * 0.5) + 0.5), ((_cse1 * 0.5) + 0.5));
}
#version 300 es
precision highp float;
precision highp int;
layout(std140) uniform Uniforms {
float time;
vec2 resolution;
float cells;
} U;
vec2 hash2(vec2 c) {
vec2 _cse0 = vec2(dot(c, vec2(127.1, 311.7)), dot(c, vec2(269.5, 183.3)));
return fract((vec2(sin(_cse0.x), sin(_cse0.y)) * 43758.5453));
}
in vec2 uv;
layout(location = 0) out vec4 _ret;
void main() {
float _licm0 = U.time;
float _av0 = 8.0;
vec2 _cse0 = (uv * U.cells);
for (int _v0 = -1; (_v0 <= 1); _v0 = (_v0 + 1)) {
for (int _v1 = -1; (_v1 <= 1); _v1 = (_v1 + 1)) {
vec2 _v2 = vec2(float(_v1), float(_v0));
vec2 _v3 = hash2((floor(_cse0) + _v2));
_av0 = min(_av0, distance(fract(_cse0), ((_v2 + ((_v3 * 0.5) + 0.25)) + (vec2(sin((_licm0 + (_v3.x * 6.283))), cos((_licm0 + (_v3.y * 6.283)))) * 0.18))));
}
}
_ret = vec4(((vec3((_av0 * _av0)) * vec3(0.35, 0.6, 1.0)) + vec3(0.02, 0.03, 0.06)), 1.0);
}

Voronoi, drawn at build time.

The WGSL and GLSL tabs are the compiler's own output at commit 26de7be8, read from the goldens its emit suite bakes (emit-goldens.test.ts).

This example is written against the fn() builder API, which the editor in the Playground does not take.

File on GitHub

Edit this page Report a problem