Shader art (plasma)
The demoscene plasma: four sines summed over the pixel’s position and the time, one of them radial from a drifting centre, pushed through a palette made of three more sines. Everything happens in the fragment shader; the rest of the document is the standard fullscreen scaffold.
; Shader art (plasma). A fullscreen triangle running a classic sine plasma:; summed sines over screen position and time, mapped through a colour palette.; Driven by a pngine-inputs uniform; no vertex buffer.
(shader-module :name shader :code """ struct Uniforms { time: f32, width: f32, height: f32, aspect: f32, } @group(0) @binding(0) var<uniform> u: Uniforms;
const PI: f32 = 3.14159265359;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f { let x = f32(i & 1u) * 4.0 - 1.0; let y = f32((i >> 1u) & 1u) * 4.0 - 1.0; return vec4f(x, y, 0.0, 1.0); }
@fragment fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f { let uv = vec2f(pos.x / u.width, pos.y / u.height); let t = u.time;
// Classic plasma effect let p = uv * 8.0;
var v = 0.0; v += sin(p.x + t); v += sin(p.y + t * 0.7); v += sin((p.x + p.y) + t * 0.5);
let cx = p.x + 0.5 * sin(t * 0.5); let cy = p.y + 0.5 * cos(t * 0.3); v += sin(sqrt(cx * cx + cy * cy + 1.0) + t);
v = v * 0.5;
// Color palette let r = sin(v * PI) * 0.5 + 0.5; let g = sin(v * PI + PI * 0.666) * 0.5 + 0.5; let b = sin(v * PI + PI * 1.333) * 0.5 + 0.5;
return vec4f(r, g, b, 1.0); } """)
(render-pipeline :name pipeline :layout auto (vertex :module shader :entry vs) (fragment :module shader :entry fs (target :format preferred-canvas-format)))
(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeUniforms (write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(bind-group :name uniformsBindGroup :layout pipeline :group 0 (entry :binding 0 :buffer uniforms))
(render-pass :name mainPass (color-attachment :view context-current-texture :clear-value [0 0 0 1] :load-op clear :store-op store) :pipeline pipeline :bind-groups [uniformsBindGroup] (draw :vertex-count 3))
(frame :name main :perform [writeUniforms mainPass])examples/samples/02_shader_art.sjon in the pngine repository.
How it works
Section titled “How it works”The document is SJON, the S-expression format pngine compiles: each form is
one WebGPU resource or operation, and the shader text inside
(shader-module …) is plain WGSL, WebGPU’s shading language.
The scaffold
Section titled “The scaffold”The same seven forms as Gradient background:
a shader module, a (render-pipeline …) with :layout auto and one colour
target in the canvas format, a 16-byte uniform buffer that a
(queue …) form fills each frame from pngine-inputs (the built-in
time/width/height/aspect source), a bind group for it, a
(render-pass …) that clears and draws three vertices, and a (frame …).
One difference from the gradient page: vs returns only
@builtin(position), and fs takes that same builtin back as its input.
The fragment stage’s position is in framebuffer pixels, so fs divides by
u.width and u.height to get a [0, 1] uv. That is why the uniform
buffer carries the canvas size at all: no interpolated uv crosses the
stages.
The plasma
Section titled “The plasma”With p = uv * 8, the shader accumulates:
sin(p.x + t): vertical bands drifting sideways,sin(p.y + 0.7 t): horizontal bands at a different rate,sin(p.x + p.y + 0.5 t): diagonal bands,sin(sqrt(cx² + cy² + 1) + t): rings around a centre that itself moves on a Lissajous path (0.5 sin(0.5 t),0.5 cos(0.3 t)).
The sum, halved, is a scalar in roughly [-2, 2]. The palette maps it to
RGB with sin(v π + phase) * 0.5 + 0.5 at phases 0, 2π/3 and 4π/3, the
same three-sines rainbow as the gradient sample. Because the four terms have
incommensurate rates the picture never loops.
In the specifications
Section titled “In the specifications”| What the sample uses | WebGPU | WGSL |
|---|---|---|
| Fullscreen triangle, one draw of three vertices | draw(), rasterization |
vertex_index |
| Fragment position in framebuffer pixels | fragment processing | position (fragment input) |
| Uniform buffer with time and canvas size | writeBuffer(), GPUBufferUsage.UNIFORM |
uniform address space, const declarations (PI) |
| Pipeline and canvas format | render pipeline creation, getPreferredCanvasFormat() |
entry points |
| The math | sin, sqrt, var and let (var v accumulates) |
Related
Section titled “Related”- Gradient background explains the scaffold form by form.
- Color cycling and Scene transitions reuse this plasma as one of several patterns.
- Forms:
(shader-module …),(render-pass …),(queue …).