IMGD/CS 4300 · D-Term 2026 · Performance recorded in wgsl_live
The shader runs entirely inside wgsl_live, a WebGPU fragment-shader
live-coding environment. Every pixel is computed per-frame in a single @fragment
function. The core visual engine is a frame-feedback loop:
lastframe() samples the previous frame through a slowly rotating UV transform
(rotate()), creating an accumulating echo-like trail that never quite repeats.
A Lissajous-drifting circle (sin/cos on the
centre point, soft edge via smoothstep) acts as the seed shape, while a
polar ripple field (length, atan2, nested
sin/cos) generates the radiating wave texture underneath.
Live audio drives the feedback weight via audio[0] (bass) and expands
the circle radius via audio[2] (highs). pow + scalar decay
prevent unbounded brightness accumulation, and a radial smoothstep vignette
darkens the edges. Mouse position shifts the feedback UV offset in real time.
The piece is interested in memory as texture: each frame is not erased but rotated and folded back into itself, so the image accumulates its own history the way an echo chamber accumulates sound. The drifting circle is less a shape than a cursor — something that marks time by leaving a residue. The colour feels metabolic rather than designed: R/G/B channels drift at incommensurable frequencies so the palette is never the same twice, cycling through warm amber, cold teal, and brief flashes of magenta.
During the performance I treated the mouse as a physical instrument — small lateral movements shear the feedback field, breaking rotational symmetry and introducing a sense of breath or instability. Audio reactivity was kept subtle: the image should feel alive to sound without obviously pulsing on the beat.
// A2 Shader Live Coding — Echo Pulse // IMGD/CS 4300, D-Term 2026 · Ctrl+Enter to reload @fragment fn fs( @builtin(position) pos : vec4f ) -> @location(0) vec4f { let t = seconds(); let uv0 = uvN( pos.xy ); // 0..1 var p = uv( pos.xy ); // -1..1 var centre = vec2f( sin(t*1.1)*.28, cos(t*.85)*.28 ); let d = distance( p, centre ); let radius = .09 + audio[2] * .06; let circle = 1.0 - smoothstep( radius, radius+.03, d ); let r = length( p ); let angle = atan2( p.y, p.x ); let ripple = sin( r*14.0 - t*2.5 + cos(angle*3.0+t) )*.5+.5; let rot = rotate( uv0, t/7.0 + sin(t*.3)*.15 ) + mouse.xy*.06; let fb = lastframe( clamp(rot, vec2f(0.), vec2f(1.)) ); let bass = .42 + audio[0] * .65; let mid = abs( sin(t*.6) ) * .15 + audio[1] * .1; var col = fb * bass; col.r += ripple * (.10 + mid); col.g += ripple * abs( sin(t*.45) ) * .08; col.b += (1.0 - ripple) * .07 + mid * .5; let stamp = vec4f( mix(.3, .95, fract(t*.18)), mix(.6, .2, fract(t*.11)), mix(.8, .4, fract(t*.23)), 1.0 ); col += vec4f(circle) * stamp; col = pow( clamp(col, vec4f(0.), vec4f(1.)), vec4f(1.018) ) * .965; col *= smoothstep( 1.1, .25, r ); return clamp( col, vec4f(0.), vec4f(1.) ); }
| # | Function | Source | Role in shader |
|---|---|---|---|
| 1 | uvN() | wgsl_live | pixel → 0..1 UV |
| 2 | uv() | wgsl_live | pixel → -1..1 centred |
| 3 | seconds() | wgsl_live | elapsed time driver |
| 4 | rotate() | wgsl_live | spin feedback UV each frame |
| 5 | lastframe() | wgsl_live | previous frame → echo trail |
| 6 | audio[] | wgsl_live | bass / mid / high frequency |
| 7 | sin() | WGSL | centre drift, ripple, colour tempo |
| 8 | cos() | WGSL | centre drift, nested ripple |
| 9 | distance() | WGSL | circle SDF |
| 10 | smoothstep() | WGSL | soft edge + vignette |
| 11 | length() | WGSL | polar radius |
| 12 | atan2() | WGSL | polar angle |
| 13 | clamp() | WGSL | UV guard + colour clip |
| 14 | abs() | WGSL | unsigned modulation |
| 15 | mix() | WGSL | colour interpolation |
| 16 | fract() | WGSL | cyclic colour channels |
| 17 | pow() | WGSL | gamma decay on feedback |
| 18 | step() | WGSL | available for live edits |
[Placeholder — fill in after collecting peer feedback] A classmate noted ...