Procedural skybox
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.
; 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])examples/samples/14_skybox.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 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 …).
From pixel to sky direction
Section titled “From pixel to sky direction”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.
Layers of sky
Section titled “Layers of sky”- 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:
sunDircircles slowly and bobs;sunDistis the chord to it, and two exponentialsexp(-8 d)andexp(-2 d)give the disc and the glow, added in warm whites. - Clouds:
fbmis four octaves of 3D value noise (noiseinterpolates eight corner hashes), sampled atdir * 2scrolled by time and scaled by 3 insidefbm.smoothstep(0.4, 0.7, density)cuts the cloud shape, and the cloud colour is lit bydot(dir, sunDir). - Sunset:
horizon²times how much the horizontal part ofdirpoints at the sun, in orange. - Tone map:
color / (color + 1)(Reinhard) compresses the bright sun back into range.
In the specifications
Section titled “In the specifications”| 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 |
Related
Section titled “Related”- Procedural noise shows the noise functions in isolation; Procedural normal mapping uses the 2D version.
- Forms:
(shader-module …),(render-pass …),(queue …).