Skip to content

Shader art (plasma)

Download PNG

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.

examples/samples/02_shader_art.sjon
; 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])

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 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.

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.

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)