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 scaffold
Section titled “The scaffold”The SJON (the S-expression format pngine compiles; each form is one WebGPU
resource or operation) is 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 …).
The plasma itself is all WGSL, WebGPU’s shading language, in the fragment
entry point.
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. I picked four rates that
never line up for long, so the picture never settles into a visible loop.
An exercise: delete the four v += lines one at a time and recompile. Each
sine is one visible ingredient (vertical bands, horizontal bands, diagonals,
rings), and the plasma is nothing but their sum.
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 …).