Sanctum · a hands-on field guide

How Sanctum's Clouds Work

The building blocks of the Sanctum sky — explained in plain English, with the math and a live cloud you reshape with your own hands.

Every interactive cloud here is a real GPU render of the Sanctum cloud — the sliders scrub through pre-rendered frames, so it runs smoothly on any device — no heavy GPU needed.

A towering volumetric cloudscape — the Sanctum production cloud rendered on the GPU
Sanctum's production cloud, rendered on the GPU — a port of Bonkahe's SunshineClouds2. The formulas below are the building blocks of this image — each section is a simplified take on one piece of the pipeline — and you can reshape a live version yourself.
↓ now drag it yourself — scrub the real cloud
Start here. Drag the sun down the sky and watch the Sanctum production cloud relight. As the sun drops, its light must cross far more air — the airmass \(m\) below — which scatters the blue away and leaves the warm sunset you see.
\( m = \dfrac{1}{\sin\,\htmlClass{kx-h}{h}} \)
Sun height

The big picture

One pixel, five steps

Every dot on your screen is a single ray fired from the camera into the world. That one ray does five things, in order. The rest of the page zooms into each — and tells you who invented it.

01
Pick a sky colour
for the ray's direction — maybe catch the sun
02
March the cloud
step through the volume, gather density & light
03
Add the air
haze & blue between you and the cloud
04
Tonemap
squeeze raw light into a viewable picture
05
Bloom
let the bright bits glow

Step 0 · the camera

How a pixel becomes a ray

The camera sits at one point. Each pixel looks in a slightly different direction — centre dead ahead, edges tilting outward. How far they tilt is the field of view. One line:

\[ \mathbf{dir}=\text{normalize}\Big(\mathbf{forward}+x\,\tan(\tfrac{fov}{2})\,\text{aspect}\,\mathbf{right}+y\,\tan(\tfrac{fov}{2})\,\mathbf{up}\Big) \]
▸ drag the field of view — watch the rays fan out
▸ the camera obscura — why a pinhole flips the image (Mozi, ~400 BCE)
Every point of the world maps to one straight ray through the aperture. Top lands low, bottom lands high — so the image inverts. That single fact is perspective projection.

Step 1 · the backdrop

The sky — two colours and a sun

Before any cloud, the ray gets a sky colour: a blend between a top and a horizon colour, mixed by how high the ray points. The 4th-power curve keeps blue across the dome and crushes the pale horizon into a thin band.

(You already dragged the sun across this cloud up in the hero — that's this exact \(\mathbf{sky}=\text{mix}\big(\text{top},\text{horizon},(1-t)^4\big)\) at work.) The blend curve is what shapes it:

▸ the blend curve \((1-t)^n\)
▸ the sun falloff \(\cos^{p}\theta\) — how tight is the glow?

The colour-space bug that made everything white a real mistake

Sky colours are authored in sRGB (your monitor's space); lighting math must run in linear light. We first fed raw sRGB numbers in as if linear — and the whole scene went much too bright — an "ungodly bright" white-out. The fix is the sRGB→linear decode \(\left(\tfrac{c+0.055}{1.055}\right)^{2.4}\).


Step 2a · the raw material

The noise library — clouds out of static

Cloud shape comes from noise — random-but-smooth patterns, sampled at different scales and stacked. Two famous noises do the work, and they fix each other's weaknesses.

▸ live noise — Perlin is too round, Worley too broken; combined they make cloud
Perlin (fBm)
soft, connected — but blobby
Worley (cellular)
lumpy — but fragmented
Perlin–Worley
Worley erodes Perlin → cloud!
▸ octaves of 1-D noise summing into a natural profile

Step 2b · shaping the cloud

Density — how much cloud is here?

One function answers "how solid is the cloud at this point?" (0–1), blending the noise through a few dials. Here are the big three, each driving the real cloud.

Coverage — the master "how cloudy" dial

Coverage — how much of the sky fills with cloud. Defined as a "water level" the noise must rise above. Means: raise it and more noise becomes cloud — clear → scattered → overcast.
\( d=\text{noise}-(1-\htmlClass{kx-cov}{\text{cov}}) \)
Coverage
▸ the coverage "water level" over a noise field (the math view)

Sharpness & density — wispy vs firm, thin vs thick

Sharpness \(p\) — how crisp vs soft the cloud is. Defined as a power on the density. Means: low values puff it into soft billows; high values thin it to wispy haze.
\( d \to d^{\,\htmlClass{kx-p}{p}} \)
Sharpness

The real cloud also uses remap (carving one shape out of another) and smoothstep (soft edges instead of hard cutoffs) — the math views:

▸ remap — slide the floor, watch the body erode
▸ hard step vs linear vs smoothstep vs smootherstep
A hard cutoff aliases into jagged rims; the smooth S-curve \(3t^2-2t^3\) feathers cloud edges. From Hermite; Perlin used it as his noise fade.

Step 2c · walking the volume

The raymarch — sampling along the ray

We walk forward in steps, asking "how much cloud?" and adding it up. The number of steps is the quality dial — and you can see it directly:

REAL GPU RENDER
Density silhouette vs fully lit in the real production cloud
◀ what the march finds (density only)+ the lighting below ▶
The Sanctum production cloud: the raw density the raymarch accumulates (left) vs the same density once it's lit (right).
↓ now drive the step count yourself, live (watch it band, then smooth)
Steps — how many samples we take along each ray. Means: too few and the cloud bands into ugly slices; more steps melt it smooth — but every step costs time. The quality-vs-speed tradeoff every real-time renderer fights.
\( \Delta=\dfrac{\text{ray length}}{\htmlClass{kx-N}{N}\ \text{steps}} \)
Steps

But what is each step actually doing? Watch one ray creep in — it peeks at the cloud bit by bit and adds up what it finds:

▸ One ray, marching in step by step — peek, add up, stop when solid (press ▶ replay)
How to read it: your eye (left) creeps along its line of sight in little steps. At each dot it asks "how much cloud is right here?" — orange = inside the cloud, so it adds to the total; faint grey = clear air, nothing to add. The instant the total reads solid, it stops — you can't see past a solid cloud, so looking further (the dashed part) would be wasted work. Thicker cloud → it goes solid sooner; more steps → finer adding-up → the smoother result you dialed just above.

Step 2d · the light

Lighting — what makes a cloud look real

Where a grey blob becomes a sunlit cloud. Four pieces of physics — and you can drive the three biggest.

1 · Beer's Law — bright edges, dark cores

REAL GPU RENDER
A fully lit cloud in the real production render
Beer's-law self-shadow in the Sanctum production cloud — bright sunlit rims, deep dark cores, the soft falloff between. (Real cumulus cores glow grey, because light bounces many times inside them. Simulating every bounce is too slow in real time, so Sanctum approximates it with Beer's-law absorption plus an ambient term — the same fast, well-established trade real-time cloud renderers all make: it reproduces the look without tracing every bounce.)

Absorption \(k\) — how fast cloud swallows light, the rate in \(T=e^{-kx}\). High \(k\) = dark dense cores; low \(k\) = thin and translucent. ↓ drag \(k\) on the live curve below and watch the survival rate plunge.

▸ Beer's Law \(e^{-kx}\) — the math view

2 · Henyey–Greenstein — the silver lining

Anisotropy \(g\) — how strongly light keeps going forward through droplets. Means: turn it up and the sun-facing edge lights up with a bright silver rim.
\( \text{HG}=\dfrac{1-\htmlClass{kx-g}{g}^2}{4\pi\,(1+\htmlClass{kx-g}{g}^2-2\,\htmlClass{kx-g}{g}\cos\theta)^{3/2}} \)
Anisotropy g
▸ scattering lobes (polar) — sun from the right; reach = brightness that way
Henyey–GreensteinRayleigh (air)Mie-like (haze)

3 · The moving sun (& the warm point light)

The same sun you dragged across the cloud in the hero is its key light — low for dramatic, reddened side-lighting; high for bright noon. The sun is white; a separate orange point light at \(\hat{\mathbf{s}}=(\cos e\sin a,\ \sin e,\ \cos e\cos a)\) warms the towers from within.


Step 3 · the air

Atmospherics — depth and the blue of the sky

Between you and a far cloud are kilometres of air that scatter light. The nearer-vs-farther fade is aerial perspective — and it's what gives a flat scene depth:

REAL GPU RENDER
Atmosphere off vs on in the real production cloud and Godot
Godot (target)port — atmosphere OFFport — atmosphere ON
The real production cloud with atmosphere off (flat, pasted-on) vs on (distant cloud recedes into haze — depth).
↓ now drive the haze yourself, live
Aerial perspective — how much distant cloud fades into the hazy air. Means: at 0 the far clouds stay crisp and flat; turn it up and they melt into the blue horizon, and the sky gains depth.
\( \text{haze}=1-e^{-\htmlClass{kx-haze}{\sigma}\,d} \)
Aerial haze
▸ scattering strength across colour — the main reason the daytime sky is blue
Heads up: Sanctum's sky doesn't simulate this — it reproduces the result with a tuned two-colour gradient, the standard real-time shortcut and far cheaper. The \(1/\lambda^4\) law is just why the colour it's aiming for is blue in the first place.

Step 4 · making it viewable

Tonemap — squeezing HDR light into a picture

Lighting produces values past 1.0 (the sun, sunlit tops). A monitor maxes at 1.0, so naive output clips to white. The filmic curve rolls highlights off like film instead:

Exposure — how much light we feed the film curve, \(\text{out}=f_{\text{filmic}}(\text{light}\times\text{exposure})\). Push it up and a naive renderer blows the sunlit tops to flat white; the filmic curve rolls them off. ↓ drag exposure on the live curve below and watch the naive (red) line clip while the filmic (orange) one keeps detail.

▸ filmic curve vs naive clipping — the math view

Step 5 · the glow

Bloom — letting the bright bits bleed

Real lenses — and your own eyes — bleed light outward from very bright areas. We copy just the bits above a brightness threshold, blur them (a Gaussian blur), and add that glow back on top:

REAL GPU RENDER
Bloom off vs on in the real production cloud
◀ bloom OFFbloom ON ▶
The Sanctum production cloud — in production bloom is kept deliberately subtle (so the off/on is faint by design). Drag the slider below to see what it actually does, pushed well past production.
↓ dial the glow yourself — applied live to a real bloom-off frame
Bloom — copy the brightest bits, blur them, add them back as a glow. Means: at 0 the render is crisp; turn it up and the bright cloud tops bleed a soft halo — like a lens flaring, or squinting at the sun.
\( \text{out}=\text{colour}+\htmlClass{kx-bi}{\text{intensity}}\times\text{blur}\big(\max(\text{colour}-\text{thresh},0)\big) \)
Glow

Bloom — \(\max(\text{colour}-\text{thresh},0)\to\text{blur}\to\text{add}\). Only the bright cloud tops cross the threshold, so the glow lands on the edges without washing out the rest.


The fourth dimension

Animation — making the sky breathe

Each noise octave drifts with the wind at its own speed, so the clouds evolve rather than slide. Here it is in the Sanctum production sky — churning in place, and flown through:

REAL GPU RENDER
Time-lapse of the real cloud evolving
Fixed camera, time-lapsed — the real cloud churns and boils in place as each noise octave drifts.

Animation — \(\text{pos}\mathrel{+}=\text{wind}\times\text{speed}\times dt\). Each octave drifts at its own speed, so the cloud churns and evolves rather than sliding past — the schematic below shows why.

▸ four layers, four speeds — why differential motion reads as "evolving"

The receipt

Does the port match the original? — the Sanctum port vs the engine

The cloud you've been dragging is no stand-in — those are real frames from the Sanctum production renderer, a full GPU raymarcher, pre-rendered so the page stays light (only the little math diagrams are drawn live in your browser). And that renderer is itself a port of Bonkahe's open-source Godot shader — so to check the port against the original, the same camera is rendered in both engines and compared side-by-side on the GPU:

Godot vs the Sanctum port, same camera, in motion
GODOT — the real engine (target)SANCTUM PRODUCTION PORT — WebGL ▶
Same vantage, in motion. Recipe and lighting match closely; the one known difference is the noise implementation (baked Worley vs live FastNoiseLite), so individual cloud shapes land in slightly different places.

The whole machine, in one breath

Sky gradient → multi-octave Perlin–Worley density → coverage & remap carving → adaptive raymarch → Beer's-law self-shadow + Henyey–Greenstein rim + ambient + sun → aerial perspective → filmic tonemap → bloom → wind animation.

Every layer is one small, checkable formula — most a century or more old — stacked on the last. You just dragged the main ones.

real GPU framesscrubbed, not recomputedruns on any deviceopen-source port

Sanctum vs the original

A port of Bonkahe's shader — and what's Sanctum's

Straight answer: the cloud recipe isn't Sanctum's to claim. It's a port of Bonkahe's open-source SunshineClouds2 (Godot, MIT) — every constant matches Bonkahe's scene, checked against the actual engine. That was the point: port it as-is and check it works. What's Sanctum's is the engineering that got those clouds running — true to the original look — somewhere they were never built to run:

① A compute shader, rebuilt as a web raymarch

Bonkahe's clouds are a Godot/Vulkan compute shader. The Sanctum version is a WebGL2 fragment raymarch — a different architecture (explicit textureLod, per-ray LOD-scaled marching, no implicit derivatives) to run the same math in a browser.

② Making a Vulkan shader behave on the web

Vulkan tolerates things WebGL/ANGLE don't. Sanctum found and fixed four classes of bug that turned the clouds into NaN-black on Direct3D — a reversed-edge smoothstep, a 0/0 in remap, undefined texture LOD in a divergent loop — plus the catch that ANGLE's fast-math silently eats isnan guards, so bad values must die at the source.

③ It runs on anything — no Godot

Bonkahe's version needs the Godot engine installed. The Sanctum port is a single WebGL2 page: the same clouds on a phone, in a browser, no install — which is the whole reason Sanctum, a three.js MMO heading to Steam, can actually use them.

④ Bonkahe's exact data, re-baked

Sanctum bakes Bonkahe's Godot noise resources into RGBA8 3-D volumes, so the inputs stay Bonkahe's, not an approximation. (The one real visual difference: Bonkahe animates FastNoiseLite; Sanctum marches baked Worley — same recipe, shapes land in different places.)

⑤ Checked against the engine, not just asserted

Sanctum built a Godot ground-truth harness that renders Bonkahe's actual engine, then put the same camera side-by-side in both on the GPU. Recipe and constants match, and the look lines up closely (that's the comparison above).

⑥ Where it's heading: a living world

The port proved Bonkahe's clouds work in three.js. Next they get Sanctum's day-night sun-and-moon and world systems driving them — that integration is Sanctum's to build.

So: the cloud math is Bonkahe's, ported and checked side-by-side against Bonkahe's engine. What Sanctum built is the bridge that makes it run as an MMO using three.js.


Sources & further reading

Further reading on the techniques used:

Built for Sanctum · the interactive sliders scrub pre-rendered frames of the real production cloud and recompute small illustrative formulas in your browser — no heavy GPU needed (see ISOLATION.md). All imagery is a GPU render.