Skip to content

Resolution-adaptive rendering

Download PNG

A shader that knows how big its canvas is. The grid lines are two pixels wide whatever the resolution, the five glowing circles stay round whatever the aspect ratio, and the tinted border reports which class the canvas is in: green for square-ish, orange for wide, blue for tall. The player above is wide, so the border is orange; the runtime resizes the canvas with the page, so narrow the window and watch it change.

examples/samples/28_resolution_adaptive.sjon
; Resolution-adaptive rendering: a fullscreen triangle whose picture adapts to
; the canvas: an aspect-corrected grid with a line width fixed in pixels
; (2 / min(width, height)), five orbiting glowing circles that stay circular at
; any aspect ratio, and a border tint that indicates the aspect class (green
; square, orange wide, blue tall). Reads width/height/aspect from the
; pngine-inputs uniform.
(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);
}
fn sdCircle(p: vec2f, r: f32) -> f32 {
return length(p) - r;
}
@fragment
fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
// Normalized device coordinates (-1 to 1)
let ndc = vec2f(
(pos.x / u.width) * 2.0 - 1.0,
1.0 - (pos.y / u.height) * 2.0
);
// Aspect-corrected coordinates (circles stay circular)
var p = ndc;
if (u.aspect > 1.0) {
p.x *= u.aspect;
} else {
p.y /= u.aspect;
}
let t = u.time;
// Background gradient
var color = vec3f(0.05 + ndc.y * 0.03, 0.05, 0.08 + ndc.x * 0.02);
// Adaptive grid (line width scales with resolution)
let minDim = min(u.width, u.height);
let gridSize = 0.15;
let lineWidth = 2.0 / minDim;
let gx = abs(fract(p.x / gridSize + 0.5) - 0.5);
let gy = abs(fract(p.y / gridSize + 0.5) - 0.5);
let gridDist = min(gx, gy) - lineWidth;
let gridColor = vec3f(0.15, 0.18, 0.22);
color = mix(gridColor, color, smoothstep(0.0, lineWidth, gridDist));
// Animated circles (always circular regardless of aspect)
for (var i = 0; i < 5; i++) {
let fi = f32(i);
let angle = t * (0.4 + fi * 0.1) + fi * PI * 0.4;
let orbRadius = 0.2 + fi * 0.1;
let center = vec2f(cos(angle), sin(angle)) * orbRadius;
let circleRadius = 0.06 + sin(t * 2.0 + fi) * 0.02;
let d = sdCircle(p - center, circleRadius);
// Glow effect
let glow = 0.015 / (abs(d) + 0.008);
let hue = fract(fi * 0.2 + t * 0.05);
// Simple HSV approximation
let c = clamp(vec3f(
abs(hue * 6.0 - 3.0) - 1.0,
2.0 - abs(hue * 6.0 - 2.0),
2.0 - abs(hue * 6.0 - 4.0)
), vec3f(0.0), vec3f(1.0));
if (d < 0.0) {
color = c;
}
color += c * glow * 0.25;
}
// Corner indicators showing aspect ratio
let corner = max(abs(ndc.x), abs(ndc.y));
if (corner > 0.92) {
var indicator = vec3f(0.2, 0.6, 0.2); // Green: square-ish
if (abs(u.aspect - 1.0) > 0.1) {
if (u.aspect > 1.0) {
indicator = vec3f(0.6, 0.3, 0.1); // Orange: wide
} else {
indicator = vec3f(0.1, 0.3, 0.6); // Blue: tall
}
}
color = mix(color, indicator, 0.4);
}
return vec4f(color, 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 fullscreen scaffold from Gradient background: one shader module, a (render-pipeline …) with :layout auto targeting the canvas format, a 16-byte uniform buffer refilled every frame from pngine-inputs (the built-in time/width/height/aspect source), a bind group, a (render-pass …) drawing three vertices, and a (frame …). This sample leans on all four uniform fields (so do spinning shapes and normal mapping; most samples read only time and the size).

ndc maps the pixel to [-1, 1] on both axes regardless of shape, so |ndc.x| and |ndc.y| near 1 mean “near the edge”: that is what the border test uses. p is the aspect-corrected copy: on a wide canvas p.x is scaled by aspect, on a tall one p.y is divided by it, so a unit of p is the same number of pixels on both axes and circles drawn in p are round.

lineWidth = 2 / min(width, height): two pixels expressed in p units. gridDist is the distance to the nearest grid line (abs(fract(p / gridSize + 0.5) - 0.5) on each axis, take the smaller) minus that width, and smoothstep(0, lineWidth, gridDist) fades from line colour to background over one more line width. Double the resolution and the lines stay two pixels wide instead of doubling in thickness.

Five circles orbit at different radii and rates, each pulsing in size with a sine; inside d < 0 the colour is the circle’s hue, and a glow 0.015 / (|d| + 0.008) is added around it. The hue comes from a compact piecewise-linear HSV approximation clamped to [0, 1]. Finally, pixels with max(|ndc.x|, |ndc.y|) > 0.92 are mixed 40% towards the aspect-class colour: green unless aspect differs from 1 by more than 0.1, then orange above 1 and blue below.

What the sample uses WebGPU WGSL
Canvas size reaching the shader writeBuffer(), canvas configuration uniform address space
Fullscreen triangle and one draw draw(), normalized device coordinates vertex_index, position
Grid and circles fract, abs, min, max, smoothstep, length, for statement
Colour and border clamp, mix, if statement