Skip to content

Procedural skybox

Download PNG

A sky with no texture in it. The fragment shader turns each pixel into a direction on a sphere, rotates that direction slowly, and builds the colour from a zenith-to-horizon gradient, a sun with a glow, clouds from 3D fractal noise, a sunset tint where the view grazes the horizon towards the sun, and a final tone-map. Despite the name there is no cubemap and no sampler; the whole sky is arithmetic.

examples/samples/14_skybox.sjon
; Procedural skybox: an animated sky (gradient, sun with glow, fbm noise
; clouds, sunset tint near the horizon, tone-mapped) painted by a fullscreen
; triangle whose fragment shader turns screen UV into a slowly rotating
; spherical view direction. Despite the name there is no cubemap and no
; texture; the sky is entirely procedural. Driven by a pngine-inputs uniform.
(buffer :name uniforms :size 16 :usage [uniform copy-dst])
(queue :name writeUniforms
(write-buffer :buffer uniforms :offset 0 :data pngine-inputs))
(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 hash(p: vec3f) -> f32 {
var p3 = fract(p * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
fn noise(p: vec3f) -> f32 {
let i = floor(p);
let f = fract(p);
let u = f * f * (3.0 - 2.0 * f);
return mix(
mix(
mix(hash(i + vec3f(0.0, 0.0, 0.0)), hash(i + vec3f(1.0, 0.0, 0.0)), u.x),
mix(hash(i + vec3f(0.0, 1.0, 0.0)), hash(i + vec3f(1.0, 1.0, 0.0)), u.x),
u.y
),
mix(
mix(hash(i + vec3f(0.0, 0.0, 1.0)), hash(i + vec3f(1.0, 0.0, 1.0)), u.x),
mix(hash(i + vec3f(0.0, 1.0, 1.0)), hash(i + vec3f(1.0, 1.0, 1.0)), u.x),
u.y
),
u.z
);
}
fn fbm(p: vec3f) -> f32 {
var value = 0.0;
var amplitude = 0.5;
var freq = 1.0;
var pos = p;
for (var i = 0u; i < 4u; i++) {
value += amplitude * noise(pos * freq);
amplitude *= 0.5;
freq *= 2.0;
}
return value;
}
@fragment
fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
let uv = vec2f(pos.x / u.width, 1.0 - pos.y / u.height);
// Create ray direction (simple spherical projection)
let phi = uv.x * PI * 2.0;
let theta = uv.y * PI;
let dir = vec3f(
sin(theta) * cos(phi),
cos(theta),
sin(theta) * sin(phi)
);
// Animated rotation
let angle = u.time * 0.05;
let c = cos(angle);
let s = sin(angle);
let rotDir = vec3f(
dir.x * c - dir.z * s,
dir.y,
dir.x * s + dir.z * c
);
// Sun position
let sunAngle = u.time * 0.1;
let sunDir = normalize(vec3f(cos(sunAngle) * 0.5, 0.4 + sin(sunAngle * 0.3) * 0.2, sin(sunAngle) * 0.5));
// Sky gradient
let horizon = 1.0 - abs(rotDir.y);
let skyColor = mix(
vec3f(0.2, 0.4, 0.8), // Zenith blue
vec3f(0.7, 0.8, 0.95), // Horizon light
pow(horizon, 0.5)
);
// Sun
let sunDist = length(rotDir - sunDir);
let sun = exp(-sunDist * 8.0) * 1.5;
let sunGlow = exp(-sunDist * 2.0) * 0.5;
// Clouds
let cloudPos = rotDir * 2.0 + vec3f(u.time * 0.02, 0.0, u.time * 0.01);
let cloudDensity = fbm(cloudPos * 3.0);
let cloudShape = smoothstep(0.4, 0.7, cloudDensity);
// Cloud color (lit by sun)
let cloudSunDot = max(dot(rotDir, sunDir), 0.0);
let cloudColor = mix(
vec3f(0.8, 0.85, 0.9),
vec3f(1.0, 0.95, 0.9),
cloudSunDot
);
// Atmosphere scattering (sunset colors near horizon)
let sunset = pow(horizon, 2.0) * max(dot(normalize(vec3f(rotDir.x, 0.0, rotDir.z)), sunDir), 0.0);
let sunsetColor = vec3f(1.0, 0.5, 0.2) * sunset * 0.5;
// Combine
var color = skyColor;
color += sunsetColor;
color = mix(color, cloudColor, cloudShape * 0.8);
color += vec3f(1.0, 0.95, 0.8) * sun;
color += vec3f(1.0, 0.8, 0.5) * sunGlow;
// Tone mapping
color = color / (color + vec3f(1.0));
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)))
(bind-group :name bindings :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 [bindings]
(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: a 16-byte uniform buffer refilled every frame from pngine-inputs (the built-in time/width/height/aspect source), one shader module, a (render-pipeline …) with :layout auto targeting the canvas format, a bind group, a (render-pass …) drawing three vertices, and a (frame …).

fs maps the pixel to uv with y flipped so 0 is the bottom, then to spherical angles: phi = uv.x · 2π around, theta = uv.y · π from the zenith. dir is the unit vector for those angles, so the canvas is an equirectangular view of the whole sphere. A rotation about y by time * 0.05 turns the view slowly.

  • Gradient: horizon = 1 - |dir.y| is 0 at the poles and 1 at the horizon; mix(zenith blue, horizon light, sqrt(horizon)) is the base.
  • Sun: sunDir circles slowly and bobs; sunDist is the chord to it, and two exponentials exp(-8 d) and exp(-2 d) give the disc and the glow, added in warm whites.
  • Clouds: fbm is four octaves of 3D value noise (noise interpolates eight corner hashes), sampled at dir * 2 scrolled by time and scaled by 3 inside fbm. smoothstep(0.4, 0.7, density) cuts the cloud shape, and the cloud colour is lit by dot(dir, sunDir).
  • Sunset: horizon² times how much the horizontal part of dir points at the sun, in orange.
  • Tone map: color / (color + 1) (Reinhard) compresses the bright sun back into range.
What the sample uses WebGPU WGSL
Fullscreen triangle and one draw draw() vertex_index, position
Uniform buffer, bind group writeBuffer(), bind group creation uniform address space
Direction and rotation sin, cos, normalize, vector types and swizzles (p3.yzx)
Sun, clouds, sunset exp, length, smoothstep, mix, pow, dot, for statement
Tone mapping abs, component-wise vector arithmetic