Contents

The volumetric cloud system, as built

The end-to-end state of the cloudscape after the clouds repair program (phases 1–3, 2026-08-09/10) — written so an engineer with no access to that conversation can continue the work. The design intent lives in docs/design/ATMOSPHERE_SYSTEM/README.md §7 and the two repair specs under docs/archive/agent/specs/; the per-task evidence lives in the three plans under docs/archive/agent/plans/. This file is the as-built map: what exists, what it costs, how to verify a change, and what is still open.

1. One planet, one source

Atmosphere::PlanetCoverageMap (engine/domain/atmosphere/include/SushiEngine/atmosphere/planet_coverage_map.hpp) is the single source of “where is the weather” for every tier:

  • Generated on the CPU, once per seed and once per simulated day (the ITCZ season), on a worker thread SeededWeather owns; double-buffered, revision-swapped, deterministic (Unit_PlanetCoverageMap pins it). 4096×2048 equirect RGBA8, row 0 = the north pole; r/g/b = low/mid/high étage coverage, a = convective fraction. Content = the closed-form zonal climatology
    • seeded centres (engine/domain/atmosphere/include/SushiEngine/atmosphere/synoptic_field.hpp), plus the two layers the closed form cannot carry: a spiral vortex warp around every centre (comma clouds) and three mesoscale value-noise octaves (multiplicative, so a subtropical high stays empty). Since 2026-09-28 the zonal term is read at a latitude that meanders with longitude (wavenumbers 4 and 7, up to 7°), a broad noise gathers each column’s cover into masses at 0.92 and gaps at 0.08 around the same mean, and each étage takes a share of the column’s total, 1 - (1 - total)^share with shares summing to one, so the three layers the renderer overlaps unite back to the total. Giving every étage the whole total had drawn a 0.66 storm track at 0.93 and the planet overcast from orbit.
  • Consumers. SeededWeather::column_at (hence the published lattice the cloudscape windows bake from), and the march’s planet-scale globe field (cloud_globe_envelope in engine/presentation/render/shaders/cloud_globe_field.glsl), which samples the identical bytes uploaded by a PlanetMapPass. The simulation and the render tier structurally cannot disagree about where the weather is; the old C++/GLSL twin-evaluator discipline is retired (see the superseding note in docs/design/ATMOSPHERE_SYSTEM/README.md §7).
  • Transport. The scene push set is FULL at its guaranteed 32 push-descriptor entries, so the map rides the bindless heap (set 1) like a material texture. Its heap index travels in SceneBlock.planet_prime_axis.w (+1; 0 = no map → every consumer stays on the closed-form fallback). Addressing helper: engine/presentation/render/shaders/planet_coverage.glsl.
  • Fallbacks are honest states: authored skies (StaticWeather), providers with no planet, and the frames before the first generation lands all read the closed form.

2. The carve, as it now stands (engine/presentation/render/shaders/cloud_carve_legacy.glsl)

Since 2026-09-28 the march carves only a sky with neither layer atlas bound: every genus has its own model, so with an atlas the march skips both the carve and the envelope read, and the probe reads the models’ own bounds instead of the envelope’s skip volume.

The bake (engine/presentation/render/shaders/cloudscape_field.comp) stores the low-frequency envelope (coverage, vertical profile, deck height, water); the march carves shape analytically per sample. The carve’s load-bearing semantics, each the fix for a named failure:

  • Column-anchored placement (CARVE_COLUMN_SQUASH): the placement noise’s vertical axis is squashed so coverage is a column statistic — low coverage spaces full-depth clouds out instead of shrinking them into flecks. The wavelength comes from the column’s reconstructed horizontal coverage cover = envelope / profile, never from the height-tapered envelope.
  • Étage decorrelation and étage wavelengths: the placement frame slides per étage (offsets keyed to the genus catalogue’s altitude gaps) so stacked decks stop forming aligned towers, and each étage carves at its own wavelength (low 1×, mid ~2.8×, high ~8×) so kilometres-thick middle/high decks patch into sheets and wisps instead of cutting into narrow deck-spanning towers.
  • Flat bases: the cumuliform height profile starts near full at the deck base (the condensation level is flat) and tapers only toward the top, where the billow ladder eats domes.
  • Coverage reaches the transmittance as coverage (the Jensen fix): correlated two-channel transmittance T = (1-e)·T_clear + e·T_cov, engaged (a) past the carve’s 160 km reach and (b) wherever the mip filter has made the carve statistical (unresolved = spread / (1/√12)) — both ends mass-consistent through CLOUD_ENVELOPE_MEAN_SHAPE.
  • Correlated sun depth: every light source integrates envelope-mean density, so an in-cloud sample divides the sampled depth by its local coverage (the same maximal-overlap assumption the view transmittance makes) — this is what gives a cumulus its dark flat underside.
  • Negative result on record (phase 2 plan): blending a second incommensurate carve scale — any ratio, any weights, even with an exact closed-form CDF re-uniformisation — fragments or empties the field. Repetition was fixed by the planet map’s globally unique modulation instead; do not retry the mixture.

3. Tier budgets, with measured numbers

Measured 2026-08-10 on the reference GTX 1060 6GB at 1920×1080 via the probe (below); whole-frame wall time, in-cloud viewpoint, probe scene (no game load):

Tier Cloud budget (steps near/far, light steps, cone cadence, buffer) ms/frame
High (before) 96/32, 5, every 2nd sample, 0.5 23.8
High (now) 72/24, 5, every 2nd sample, 0.5, early-out 0.03 19.4
Ultra (before) 128/48, 6, every sample, 0.75 56.7
Ultra (2026-08-10) 128/48, 6, every 2nd sample, 0.5 (+ silver lining) 32.2
Ultra (2026-09-28) 72/24, 5, every 2nd sample, 0.5 (+ silver lining) —

Since 2026-09-28 Ultra marches High’s steps: under the 2x2 amortized march, 128/48 cost about 80 % more cloud time on an RTX 3080 Ti for a 1.8-level mean difference, within the resolve’s noise. The 2026-08-10 rows predate the amortized march and the cell bake.

The cloud pass alone, measured 2026-09-28 on the RTX 3080 Ti at 1024×576 and High with cloud_capture --pass-times, after the angular step went from 0.02 to 0.03 of the distance and a cell class resolved only past four footprints:

Sky Viewpoint Cloud pass, ms (before the two changes)
Seeded ground 8.57 (11.58)
Seeded km100 8.37
Seeded high 5.25
Cumulus, 50 % ground 4.35
Cumulonimbus, 50 % ground 6.23

Removing one model at a time from the seeded ground frame measured the cells at 4.9 ms of the 11.6, cumulonimbus 1.2, the sheets 0.9 and cirrus 0.4; the cells’ sun and overhang taps are 0.7 ms of theirs. The 2 ms target is not met; the plan is docs/design/CLOUD_MARCH_BUDGET.md.

The editor at these settings profiles ~18 ms scene GPU with the cloud pass at ~10.2 ms (user’s Profiler capture, same GPU). Ground rule from the program: a tier is a cost ceiling, and no fix may raise an existing tier’s cost. The 2.4× Ultra blowup came from the every-sample cone light march plus the 0.75 buffer — both were nuances the temporal resolve largely absorbs, and the shader now maps light_taps == 3 to the every-2nd cadence on purpose (engine/presentation/render/shaders/cloud.frag’s main, tap_stride).

4. How to verify any cloud change — the probe

se render --probe clouds is the eye this entire program was steered by. Never judge a cloud change by reasoning alone, and never ask for editor screenshots when the probe can answer:

se render --probe clouds -- --viewpoint ground            # one viewpoint, all debug modes
se render --probe clouds -- --viewpoint incloud --mode off --warm 96 \
    --width 1920 --height 1080 --quality ultra            # a performance measurement
se render --probe clouds -- --viewpoint ground --sequence 24 --velocity 60,0,0
                                                          # motion: carve vs TAA attribution
  • Viewpoints: ground, zenith, high (15 km), km100, km100_down, incloud (1.8 km), orbit, space. Modes: off/envelope/carved/erosion_off/light_depth/window_weight/march_cost/carve_lod/ field_sources/field_peak_height.
  • carve_lod packs three separate readings into one frame: red is base_lod normalised against the mip chain’s last level, green is statistical (0 = the carve answered the sample directly, 1 = the sample came from the envelope mean), and blue is mean_mix (the CARVE_END distance ramp alone, with no mip contribution). The third channel exists because statistical = max(mean_mix, unresolved) hides which term won a given pixel; blue is what separates a mip-driven hand-off to the envelope mean from the intended distance one.
  • field_sources asks the three envelope sources what each of them says at one point: red the near window, green the far window, blue the planet-scale globe field. Point samples along the ray’s traversal of the shell, deliberately not an integral — the envelope view integrates and saturates on slant paths, which is the false meter below. Each source is read raw rather than blended by its rim weight, because a blended read cannot show the two disagreeing; the weight only gates whether a source answers here at all, so a clamp-to-edge fetch outside a window never reads as data. One limitation to know before trusting it: a single height cannot tell an empty source from one that puts its deck at a different altitude, so the view sweeps 16 heights across the shell and reports each source’s maximum over that column.
  • field_peak_height is its sibling: the same sweep, but each channel carries the height01 at which that source peaked rather than the value it reached. Zero means both “peaks at the shell floor” and “has no cloud in this column” — read it beside field_sources, which is why the two exist as a pair rather than as one view.
  • --clouds-off/--fog-off/--atmosphere-off/--surface-off attribute an artifact to the pass that draws it one variable at a time — this is how the “100 km milky veil” was proven to be the analytic surface path and not the cloud bake, after two wrong attributions.
  • The warm-up’s second half prints ms/frame — the number the tier table above is stated in.
  • BMPs convert to PNG for viewing with System.Drawing (PowerShell) — see the plans for the one-liner.
  • The envelope debug view saturates along slant paths. Never judge a coverage change from it; compare lit frames. (This false meter cost a full diagnosis cycle.)

Process rules that repeatedly proved load-bearing: never issue a shader edit and the build that compiles it in one parallel step (stale-binary diagnoses); the march-cost view paints early-outs magenta/yellow/cyan so “the march never ran here” is a reading, not a guess.

5. Open backlog (ordered by user pain)

  1. Rotation smear: translation is clean to 300 m/s (probe sequences), but camera rotation smear in the editor is unverified — the probe sequence mode moves, it does not yet turn. Extend --sequence with a yaw rate if this needs isolating.

  2. Orbit checker grain (the cloud TAA’s half-res checkerboard printing at planet-scale gradients) and the blocky patch at the 15 km view’s centre.

  3. km100 window-boundary arc — provably not a coverage disagreement any more (both sides read one map); suspect the rim weight or the lighting hand-off.

  4. The far field loses its variance (previously filed as the “km100_down visibility gap”, which named the symptom and mis-named the cause). At the km100_down viewpoint a sharp rectangle sits dead-centre of the screen where the near window’s footprint projects. It is the boundary between two regimes, not a hole and not a failure: inside the near window the envelope resolves gaps, outside it the far window’s envelope is a local mean that cannot contain a gap at all.

    The measurement, from field_profile (§4) at the camera’s own world column, in thirty-two steps across the shell. The far window carries three healthy decks — 84/255 at the shell floor, 91 at h01 0.30, 72 across 0.67-0.83 — with clean air between them. The near window is flat at 3-12 for the entire column, at its own centre, where its rim weight is 1 and nothing masks it. That reads as a broken near window and is not one: field_sources’ column maximum over the many columns inside the same footprint gives the near window 73 against the far window’s 89. The near window has cloud; the camera’s column happens to sit in a gap. (Retired — see the end of this item.)

    Which is the whole point. The near window’s texel is 128 m and the far window’s is 1024 m. At the lattice’s own coverage mean 0.399, three fifths of the sky is gap: a 128 m texel resolves that gap and reads 0.04 in it, while a 1024 m texel averages it away and reads the neighbourhood’s 0.35 everywhere. The two windows do not disagree about the weather. They disagree about variance, exactly as their resolutions require.

    The defect is what happens to the variance afterwards. The carve is what puts gaps back into a mean envelope, and it stops doing so with distance — statistical takes a sample to the envelope mean, and past that the far field is a gapless veil at mean opacity. The rectangle’s hard edge is the seam between a field with gaps and a field without them. This is the honest statement of the residual recorded loosely elsewhere as “the far bake’s envelope saturates”: it does not saturate, it flattens.

    It is also, in all likelihood, the standing complaint that high-altitude cloud looks uniform and repetitive. A gapless mean envelope carved at one scale yields evenly spaced blobs of one size, which is what the sky looks like from tens of kilometres up.

    Four hypotheses eliminated by measurement along the way, kept with the reason each was wrong, because each was internally consistent and each was believed:

    • The planet coverage map’s atan2 pole singularity: real, fixed, and pixel-identical before and after — not this.
    • The near/far split’s jitter freeze: unrelated code path.
    • The footprint→mip chain saturating and mis-triggering the statistical hand-off — the item’s own long-standing leading suspect. carve_lod’s red channel is low inside the rectangle and at every other viewpoint captured; base_lod never approaches the chain’s ceiling anywhere. Its green channel is low inside the rectangle too, so the carve is answering there and the artefact is not an LOD event at all.
    • The near window being empty, and its mirror image, the far window being a saturated veil. Both were read from summaries: window_weight weights by in-scatter and so reports which window served the cloud that exists, and a single-height field_sources slice caught the two windows at an altitude where only one had cloud.

    The instrument’s own lesson is worth as much as the finding. Five summaries were taken — shell-entry slice, midpoint slice, column maximum, peak height, and finally the profile — and the first four each told a different, coherent, wrong story. A shape cannot be read from a scalar per pixel; field_profile exists because the question was always about a shape.

    Retired by measurement, 2026-08-10. The multi-column profile this item wanted has been taken. Nine field_profile captures from 0 to 30 000 metres of horizontal separation at this viewpoint are tabulated in the Results section of docs/archive/agent/plans/2026_08_10_CLOUD_FAR_FIELD_VARIANCE_S0_S1.md. The near window’s column total moves from 0.914 to 0.908 across that span — 234 of its own 128-metre texels — so the camera’s column never leaves the supposed gap, and no gap in a 128-metre-texel field is 30 kilometres wide. The gap explanation above is retired, and with it the account of two windows that disagree about variance rather than about the weather. What replaces it is not “the near window is empty”, a misreading the same measurement guards against: the flatness is in the envelope across all three sources — near window, far window and planet-scale globe field vary by 2.3, 2.8 and 1.8 per cent over the whole span — and the band responsible is not yet named. This section is rewritten to the finding at stage S3 of that work.

    Still open here: the rectangle’s two-toned upper half.

  5. Surface-albedo / in-scatter brightness calibration vs the Google Earth reference: the “milky planet” decomposes into flat bright analytic land + honest coverage + moderate blue in-scatter (probe decomposition in the phase 3 plan). This is content tuning in engine/presentation/render/shaders/sky.frag’s ground branch / PlanetParameters, not a cloud defect.

  6. Named limits: the planet map does not advect with the wind (static per seed/day); when the GPU nest runs, the lattice keeps reading the nest and the map fills everything outside it.

Resolved since the table above was written:

  • Mid-distance horizontal banding on cloud faces, worst at shallow view angles — the near/far split’s jitter freeze (engine/presentation/render/shaders/cloud_march_quadrature.glsl, past JITTER_FREEZE_METERS) dropped the march’s per-pixel dither to a shared constant instead of only its temporal term, aligning every far pixel’s step-quantisation error instead of scattering it. The freeze itself is gone since 2026-09-26: a frozen per-pixel dither is temporally constant, so the cloud resolve could not average it and a still camera saw a fixed grain on every cloud edge past 250 m. The dither animates at every distance; a 16-frame ground sequence measured the same frame-to-frame flicker with and without the freeze. It advanced with the anti-aliasing jitter, which is zero with anti-aliasing off, while the cloud resolve runs in every mode, so with anti-aliasing off every cloud carried the same frozen grain. Since 2026-09-27 the march reads the frame counter, CloudBudget::frame_index, and the dither advances in every mode.
  • Equirect pole pinch, the sampling half of it: planet_coverage_sample (engine/presentation/render/shaders/planet_coverage.glsl) computed longitude via atan2 on two components that both go to zero at a pole, which is numerically arbitrary for adjacent samples exactly where the generator’s own row is already longitude-independent (engine/domain/atmosphere/include/SushiEngine/atmosphere/planet_coverage_map.hpp’s generate_row builds its pre-warp direction from cos(latitude) * trig(longitude), zero for every longitude at the pole). Now cross-fades to a fixed-column read of the same row within 3 degrees of a pole. Real and correctness-improving, but confirmed not the km100_down cause above — kept as a separate, smaller fix. The map’s own generation-side behaviour at the pole (the other half of “texels pinch”) is untouched.

6. Where things live

Thing File
Planet map (generation + sampling) engine/domain/atmosphere/include/SushiEngine/atmosphere/planet_coverage_map.hpp
Provider integration engine/world/simulation/include/SushiEngine/simulation/seeded_weather.hpp, engine/world/simulation/include/SushiEngine/simulation/weather_provider.hpp
Air mass map engine/domain/atmosphere/include/SushiEngine/atmosphere/planet_air_mass_map.hpp, the shared warp in planet_warp.hpp
Render upload engine/presentation/render/source/passes/planet_map_pass.{hpp,cpp}, one instance per map
Shadows and motion engine/presentation/render/source/clouds/cloud_shadow_placement.{hpp,cpp}, engine/presentation/render/source/passes/sky_velocity_pass.{hpp,cpp}, engine/presentation/render/shaders/cloud_reprojection.glsl
The ground under the layers engine/presentation/render/source/clouds/cloud_ground_grid.{hpp,cpp}, engine/presentation/render/source/passes/cloud_ground_pass.{hpp,cpp}, engine/presentation/render/shaders/cloud_ground.glsl
Shader addressing engine/presentation/render/shaders/planet_coverage.glsl, engine/presentation/render/shaders/scene_weather_tail.glsl (planet_prime_axis)
The march + carve engine/presentation/render/shaders/cloud.frag (the march’s main) over its include bricks: cloud_march_geometry.glsl, cloud_noise_lod.glsl, cloud_sun_sky.glsl, cloud_globe_field.glsl, cloud_field_sources.glsl, cloud_carve_legacy.glsl, cloud_march_quadrature.glsl, cloud_debug_views.glsl; the cells in cloud_cells.glsl, cloud_cumuliform.glsl, cloud_cell_density.glsl
The envelope bake engine/presentation/render/shaders/cloudscape_field.comp, engine/presentation/render/shaders/cloud_field_window.glsl
Tier budgets engine/presentation/render/source/frame/quality.cpp
The probe tools/probes/cloud_capture/main.cpp (se render --probe clouds)
Unit tests tests/unit/test_planet_coverage_map.cpp, tests/unit/test_planet_air_mass_map.cpp, tests/unit/test_cloud_shadow_placement.cpp

7. The layer atlas

CloudLayerField (engine/presentation/render/source/passes/cloud_layer_field.{hpp,cpp}) bakes each far-window column into up to three cloud layers: low, middle and high. The design is docs/archive/agent/specs/2026_09_25_CLOUD_MODEL_REBUILD_DESIGN.md §4.1.

  • Tiles. One RGBA16F image, 1536 × 256, holding six 256 × 256 tiles over the far window’s 262 144 m span, so one texel covers 1024 m. Tiles 0-2 are the low, middle and high layers: r is the base altitude and g the top altitude, both in metres above the surface. b is the coverage. a holds the genus index plus min(water / 4, 0.98). An empty layer is all zero. Tile 3 is the reach: r holds the largest water of any cellular layer (cumulus, altocumulus, stratocumulus, and the cumulus field under cumulonimbus) within three texels, g the highest any of those cells can stand at. Tile 4 is scratch for the reach’s first pass. Tile 5 holds the column’s air mass, encoded as the air mass map’s channels; the global atlas carries the same as its fourth tile. Every march, bake and shadow reader takes the air mass from these tiles through cloud_layer_air_at (cloud_layer_resolve.glsl).
  • Producers. The bake (cloud_layer_field.comp) has four sources, taken in this order. Inside the regional nest it scans the nest column at 24 levels across the march shell and splits the condensate runs into layers by their mid altitude; a run drawn as cellular cloud stands at most 3 km deep, since a cell’s height follows its layer’s depth and a deeper run raised towers of 5 to 6 km. The clouds are the nest’s only reader, so while a seeded planet map is published the scene view neither steps the nest nor hands it to this bake (VulkanSceneView, nest_places_clouds); the map places every cloud, and the nest serves a provider with no map. Where the seeded planet coverage map is bound, cloud_layers_from_map (cloud_layer_from_map.glsl) classifies the map’s column, the same function the global atlas bakes with, so the two atlases agree at the window’s rim. Where the published field classifies, it maps the low, middle and high bands onto the three layers through classify_genus, and a cumulonimbus takes over the low layer. An authored sky puts each deck on its genus’s layer; SceneBlock.cloud_deck_b[i].w carries each authored deck’s genus index for this. Every source states its bases over a flat surface, so the bake then lifts them onto the ground (cloud_ground.glsl, cloud_layers_lift): the low layer rises by the ground’s elevation, and a middle or high layer keeps its altitude unless the ground comes within 500 m of its base. The ground is Clouds::CloudGroundGrid, the loaded terrain smoothed over 4 km texels; with no terrain loaded it is sea level. Past the window the global atlas keeps sea level. Fed from the seeded field lattice instead, the window’s atlas held no layer at the probe’s default site and at offsets up to 60 km while the global atlas past its rim held cloud, so a clear sky followed the camera.
  • Addressing. A texel covers the same ground as the far envelope window’s texel: uv is (pattern_xz - cloud_field_pattern.zw) / cloud_field_params.y. The march reads a layer with texelFetch and the reach bilinearly (cloud_layer_access.glsl).
  • Cadence. CloudscapeCompilePass records the bake inside the frame that completes a far bake, right after the far sun-depth resolve. The atlas and the far window’s placement therefore change in the same frame. Two reach passes follow, one per axis (cloud_layer_reach.comp).
  • Heap lane. The scene push set is full, so the atlas sits in the bindless heap (set 1), sampled in GENERAL. SceneBlock.cloud_layer_parameters.x is its slot plus one. The word stays 0 until the first bake has been recorded and whenever the device has no heap. cloud_layer_bound() also requires a published far window, and while it returns false nothing reads the atlas.
  • Debug view. CloudDebugView::Layers (probe mode layers) paints the low, middle and high coverage met along the ray as red, green and blue.

8. Cells

Cumulus, altocumulus and stratocumulus are drawn as discrete cells, one cloud per cell, instead of by the legacy carve. The design is docs/archive/agent/specs/2026_09_25_CLOUD_MODEL_REBUILD_DESIGN.md §4.2 and §4.3.

  • Placement. cloud_cells.glsl mirrors Atmosphere::cloud_cells (cloud_cells.hpp): a Boolean model with four size classes, radii 1600, 800, 400 and 200 m on cube lattices of side 2198, 1554, 1099 and 777 m in the body-fixed pattern space (section 15). The middle layer scales them by 0.35. A cube holds a Poisson(0.94) number of candidates; one stands on the shell only within half a side of the reference sphere. The thinning ratio is r = -ln(1 - c) / -ln(0.05), where c is the layer’s coverage at the candidate’s centre times the far window’s rim weight, after clustering and streets. The clustering gain swings evenly about one and stops where the coverage would pass CLOUD_CELL_MAX_COVERAGE, so the mean coverage of a field is the layer’s. A candidate whose thinning hash falls under r times 65536 stands, and cloud_cell_growth scales its radius, and so its height, across a band 0.2 r wide: nothing at r (1 + 2/15), full size at r (1 - 1/15). A cell whose coverage crosses its threshold grows from nothing instead of appearing whole in one frame. The band sits two thirds above r because a cell’s area goes with its radius squared, so the expected area over all hashes stays r and the covered fraction stays c in expectation, up to the 0.95 cap. At 15 times the clock from the ground under 75° N cumulus, the most pixels one frame changed by over 16 levels of 255 fell from 2231 to 838.
  • Which layers. The low and middle layers are searched. A cell stands only where the atlas holds a cellular genus (cloud_genus_is_cellular: cumulus, altocumulus, stratocumulus) at its centre, and it takes that texel’s base, top and water, so one cloud has one genus and one base.
  • Shape. cloud_cumuliform.glsl gives a cell a flat base and a superellipse dome whose core is 0.8 of the radius and whose height is 0.45 to 1.15 times its width, times 0.55 for cells under 250 m rising to 1 at 1500 m, capped by the layer’s depth. The base is tucked in to 0.72 of the core and widens over the lowest 30 % of the height. A tower field, the march noise’s base-shape channel at 0.9 of the radius in the pattern frame, scales the dome’s height between 0.55 and 1.1 across the cell, so the top is several mounds. Three billow tiers of the inverted-Worley channel, at 1.0, 0.45 and 0.2 of the radius with amplitudes 0.3, 0.135 and 0.061 of it, push the surface out into turrets, growing in from 5 % to 25 % of the height; an outward push fades over 0.35 radii from the core, so no turret floats free. The silhouette may reach 1.4 radii and the top 1.43 heights. Each cell’s top leans up to 0.3 radii in a direction its hash picks. The surface ramp is a tenth of the radius, at least 30 m or one footprint, and a warped, vertically squashed strand noise at 0.07 of the radius eats up to 85 % of it: wispy strands at the base, billowy rims above 70 % of the height. Wisps also thin the lowest tenth. Water rises linearly from a quarter at the base to all of it at the top.
  • Which genus. classify_cloud_genus and its GPU twin classify_genus read a low band as cumulus above a convective fraction of 0.5, stratocumulus above 0.2, and otherwise as stratus only when its coverage exceeds sheet_coverage, 0.75. A middle band reads as altocumulus above a convective fraction of 0.5 or at or under that coverage, as nimbostratus above middle_overcast, 0.9, and as altostratus between. A broken layer is cellular; only an unbroken one is a sheet.
  • Per-genus style. cumuliform_style gives stratocumulus a sheet: aspect 0.15 to 0.35, a first billow of 0.3, towers between 0.8 and 1.0, a side exponent of 5 so the sides stand near vertical, and a base kept at 0.95 of the core. Cumulus and altocumulus keep the heaped values above. Every cell’s billows scale with the smaller of its radius and 1.5 times its height, so a thin sheet stays lumpy instead of flattening into one broad swell.
  • Placement coverage. A cell covers about 0.6 of its site’s disc seen from below, so sites are placed at 1 - (1 - c)^(1 / 0.6) for the drawn cells to cover c.
  • Fusion. Where cells overlap, their densities add up to the layer’s water, so two neighbours fuse through a fillet instead of one dome standing in front of the other.
  • Level of detail. The cells take the sample’s lateral pixel footprint, not the integration step, which is an order of magnitude coarser at range. A billow or strand tier fades out once its smallest feature, an eighth of its wavelength, falls under two footprints. A class whose largest cell spans less than four footprints is not searched: it covers 1 - exp(-λĀ) of the footprint over the lowest 60 % of its layer, with the cell fill water × 0.8 inside that share, and the march folds it through its unresolved-coverage sub-ray instead of spreading it as a veil.
  • The legacy carve steps aside. Where the sample’s own column holds a cellular genus between its base and 1.3 times its depth, whatever the texel’s coverage, the carve’s density is dropped and the cells answer alone. Every other genus keeps the carve. While cloud_layer_bound() is false the cells return zero and the march is the legacy march.
  • Lighting. A sample the cells win takes its optical depth toward the sun from the longest path among the cells it stands in. cumuliform_sun_depth takes two taps of 0.08 radii through the cell’s own density, so one billow shades the next, then the path to the exit of the half-spheroid under the local tower’s height, or to its base when the sun is below the local horizon, times the water and a mean fill of 0.8. A spheroid at the cell’s tallest height buried the top of every lower turret, which drew as a dark body studded with lit billows. A baked light volume around the camera was tried and removed: at 8 km it reached only the nearest cells and matched the analytic path there to 5 %. cloud_cell_sun_energy turns it into light with four octaves of the dual-lobe ladder, divided by their sum of 1.875 so a lit face’s albedo is one, plus a diffuse-transmission term 0.25 × (1 - e^-τ) / (1 + 0.75 (1 - 0.85) τ) that keeps a deep face grey instead of black and never grows with depth. The carve keeps cloud_sun_energy, its three octaves and its multiple-scatter floor; the light volume and the cone march light the carve only. The ambient on a cell rises from 0.35 at its base to 1 at its top.
  • Overhang. cumuliform_overhang takes two taps of 0.08 radii straight up through the cell’s own density and turns their optical depth into mix(0.35, 1, 1 / (1 + 0.1125 τ)). It scales both the skylight and the diffuse-transmission term of cloud_cell_sun_energy, so a crease under a billow keeps only the light scattered in sideways and the shaded side of a heaped cloud shows its billows. CloudOwnSample::overhang carries it, and the fold keeps the smallest; the sheets and cirrus leave it at 1.
  • Skipping. The march’s probe (cloud_march_probe) takes the larger of the envelope probe and the reach tile, and a hop stops at the edge of the reach’s dual cell, so the march never hops over a cell. The cone light march still reads the envelope probe alone.
  • Past the window. cloud_layer_resolve.glsl answers a column’s layer from the window’s atlas inside it and from the global atlas past it, dithering across the outer 0.3 of the window’s fade; every genus model reads it. Past the window every cell class enters as its mean cover, modulated by the clustering and the streets, and cloud_far_band hops a ray to the column’s cloud band and steps across it in twelve; see docs/design/CLOUD_GLOBAL_LAYER_ATLAS.md.
  • Cell bake. A CloudHeapBakePass (cloud_heap_bake_pass.{hpp,cpp}, described in cloud_heap_bakes.hpp, cloud_cell_bake.comp) runs every frame the clouds draw, right after the layer atlases and before the shadow map and the march. Its grid is a Clouds::CloudWrappedGrid (cloud_wrapped_grid.{hpp,cpp}) that Clouds::CloudCellPlacement (cloud_cell_placement.{hpp,cpp}) moves under the camera for the scene view: stored wrapped, a frame bakes the texels the grid newly covers and the stored rows the simulated time has earned, at most a sixteenth of them and a whole cycle in no less than two seconds, so a paused clock refreshes none (CloudWrappedGrid, CLOUD_CELL_BAKE_REFRESH_SECONDS). A bake of the layer atlases refreshes every row (CloudscapeCompilePass::layer_revision), since the lists are built from them: without that a paused clock kept the lists of the first frame, baked before any atlas, and no cell stood. And SceneBlock.cloud_cell_bake_map and cloud_cell_bake_refresh say which. A texel’s cells hang from its anchor, its column’s pattern position rounded to 1024 m, so a list stays true while the camera moves; its bounds widen by the drift the last refresh cycle carried, which the scene view sums from twice the wind times the clock’s steps and the frame’s turn under the grid, and publishes in the map’s w. A drift that would carry that sum past 1000 m (CLOUD_CELL_BAKE_STALE_LIMIT_METRES), or a jump past the grid, bakes every row that frame and clears the sum, so fast time never widens the scan without bound: at 3600 times the clock the bake cost 1.0 s a frame and lost the device, and now peaks at 4.4 ms at any rate. A restarted cloud frame, a new body, resets the grid. Under 75 N cumulus the bake costs 0.5 ms at 1024 by 576, where a full bake a frame cost 5.5 ms on the 3D lattice and 16.4 ms on the 2D one before it. Over a camera-centred window of 256 by 256 texels of 256 m in the pattern frame, it lists for each texel the standing cells, every class of both cellular layers, whose 1.7-radius bound reaches the texel’s square: up to 24, each in two float texels after a header, the first its centre, radius and ceiling, the second its base, height, water, genus, class, layer and lean key. A reader rejects most candidates on the first texel alone. A texel after the header holds four masks, one a quadrant, with a bit for each listed cell whose bound reaches that quadrant; a sample reads only its quadrant’s cells. A texel with more cells keeps its largest; the smallest are not drawn. cloud_cell_candidates.glsl holds the layout and cloud_cells_candidate, the acceptance test. The bake registers in the bindless heap and SceneBlock.cloud_layer_parameters.y carries its slot plus one. A sample reads its texel’s list and stands in each listed cell of a class resolved at its footprint. Only the bake resolves cells: across its outer 32 texels, 8 km, a sample resolves them only above the per-pixel resolve threshold, so they hand over to their mean cover by dithering, and past it every class is mean cover. Scanning the lattice per sample past the bake had cost 3.4 ms of an 8 ms frame from the ground. The header’s z and w hold the lowest base and the highest ceiling of the listed cells: a sample outside them reads no candidate, and where every class resolves at the probe’s footprint the march’s probe calls that air clear and hops to the texel’s edge or to the altitude the cells start at. Past the window the probe takes a column’s cloud band as dense and the air around it as clear; it no longer reads the envelope’s skip volume.
  • Cost. A scanning sample visits at most 100 grid squares per layer (25 for each class), each with 4 hashes, and 6 hashes per candidate; a candidate within 1.7 radii adds one atlas fetch and 8 hashes for the clustering noise. A standing cell adds up to 21 noise fetches: seven per density evaluation, three evaluations with the sun taps. About four in five dense samples of a cumulonimbus sky fall in clear air between cells, which is what the bake makes cheap. Seen from the ground at 30 % and 1024 by 576, a frame of cumulus costs 5.6 ms with the bake and the 2x2 amortized march; it cost 21 ms before either.
  • Debug views. CloudDebugView::Layers (probe mode layers) shows the atlas. CloudDebugView::Cells (probe mode cells) marches the cells alone, unlit, with the carve switched off.

9. Cirrus

Cirrus is drawn as a thin sheet of wind-stretched fibres by cloud_cirriform.glsl, instead of by the legacy carve. Cirrostratus is the same model as a thin veil: its mask is the layer’s coverage moved by up to a quarter by the gathering noise, not a threshold, and its fibres show through an even haze at 0.3 plus 0.7 of their strength, at a density of the water times 0.035, thin enough for the sun to show through. Cirrocumulus is the same model broken into tufts: cirrocumulus_grains thresholds the billow channel at 900 m above 0.35, read through cloud_noise_unrepeat, a warp of up to 360 m from the base-shape channel at 4.8 km, so the tufts repeat on no lattice, and gathers them on ripple crests 1.4 km apart along the drift, east, their crests bent with the flow and fading along their length with a 8.4 km base-shape field, so they read as rows rather than stripes; the fibres keep at least half of each tuft, and the density is the water times 0.05. The gathering mask at 24 km reads cloud_noise_aperiodic, the stratiform gaps’ warped field plus a broad scale 2.53 times wider, and the flow at 26 km and the crests’ 8.4 km field read through cloud_noise_unrepeat; read straight, each repeated once per wavelength, and from 100 km cirrocumulus showed a lattice.

  • Which columns. Where the atlas’s high layer holds cirrus and the sample sits between its base and top, cloud_cirrus_sample answers and the carve’s density is dropped, as for the cells. The carve’s envelope reaches one field level past a layer’s top, the shell’s depth over 32, about 430 m; cloud_own_envelope_smear gives that height, and the cirriform and stratiform models own it too, so the carve draws no flecks above their layers.
  • Shape. A gathering mask, the march noise’s base-shape channel at 24 km, thresholded at one minus the layer’s coverage with a soft edge of 0.3, decides where cirrus stands. Inside it, strands are level curves of a flow coordinate ψ: the distance across the wind, bent by two octaves of the same channel at 26 km and 9.6 km, read three mip levels coarse so the bend carries no fine wiggle, by up to 5 km. Level curves of a smooth field never break or swirl, so each strand runs unbroken and curves gently. Across ψ, cirrus_comb sets one Gaussian strand per slot, 700 m and 1700 m apart in two sets, each with a hashed offset, half-width of 0.1 to 0.4 of its spacing and brightness, and each fading in and out along its length over 30 km. A third comb, 160 m apart, striates each strand from within. ψ shifts by 600 m across the layer’s depth, so a strand slants like a fall streak. The sheet is a Gaussian band with a 250 m half-thickness whose centre rides the flow’s bend.
  • Density and light. The density is the layer’s water times 0.25, thin enough that the sky shows through. A fibre’s path toward the sun is its density over 150 m, and it takes the cells’ sun energy and the top of their ambient gradient.
  • Level of detail. A comb fades to its strands’ mean once their widest half-width falls under two lateral footprints.
  • Limit. Past the far window the atlas is empty, so the carve still draws cirrus there; near the horizon a cream band of legacy cirrus remains.
  • Cost. Seen from the ground at 50 %, cirrus cost 5.3 ms a frame on an unloaded run; the same shader read 17 to 20 ms while another process loaded the GPU.

10. Stratiform sheets

Stratus, altostratus and nimbostratus are drawn as sheets by cloud_stratiform.glsl, instead of by the legacy carve. Every genus model answers through cloud_own_sample.glsl’s CloudOwnSample, and cloud_march_sample folds the cells, cirrus and the sheets with cloud_own_fold: densities add, the densest model sets the height and coverage, and the longest sun and sky paths win. The carve is evaluated only where no model owns the sample; in a scene of cumulonimbus seen from the ground that saves about 2 ms of a 30 ms frame.

  • Which columns. Stratus is the low layer’s sheet; altostratus and nimbostratus are the middle layer’s, and blend as one sheet. The two layers are evaluated separately, so a stratus deck and an altostratus deck can stand one above the other. A layer’s sixteen-texel read runs only where its nearest texel holds a sheet genus and the sample is within half the sheet’s depth, plus the scud’s 500 m, of its band. cloud_layer_smooth reads each layer’s base, top, coverage and water through a cubic B-spline over 4x4 atlas texels, counting texels outside the layer’s sheet genera as clear, so a sheet curves between columns and thins at its edge instead of stepping by a texel.
  • Shape. Gaps open where a gap field falls under one minus the coverage, with a soft edge of 0.15. The field, cloud_noise_aperiodic in cloud_noise_warp.glsl, mixes the march noise’s base-shape channel at 12 km, warped off its period, 0.6, with the same channel at 30.4 km, 0.4, and restores the spread the mix loses; only its lowest values open gaps, and a single scale would open one per period of the noise, a lattice seen from orbit. The base swells by 18 % of the depth over 3.5 km and hangs in rounded lumps of 12 % over 900 m, the squared base-shape channel; the top undulates by 8 %. Toward a gap both faces close toward 35 % of the depth, so an edge is a lens rather than a cliff. Inside, a three-dimensional texture at 2.2 km, varying three times faster in altitude, takes up to 40 % of the density. Nimbostratus trails scud: rags of the billow channel over 30 % of the underside, up to 500 m below the base.
  • Density and light. The density is the layer’s water times 1.2. The sun path is the slab above the sample divided by the sun’s elevation sine, at least 0.1, which is exact for a level sheet, and it takes the cells’ sun energy. The skylight reaches a sample by diffuse transmission through the sheet above, 1 / (1 + 0.1125 τ_up).
  • Skipping. cloud_march_probe also reads cloud_layer_presence: any non-cellular atlas layer whose band, widened by 40 % of its depth and 600 m, holds the sample’s altitude counts as dense (the reach already bounds the cellular ones), so the march never hops over a sheet, a cirrus fibre or a cell where the legacy envelope is empty. Before this, the envelope’s skip lattice cut sheets into blocks.
  • Colour. A sunlit sheet is the sun’s own colour, about 5500 K; nothing white-balances the frame, so stratus reads warm grey.
  • Cost. Seen from the ground at 90 %, the three genera cost 2 to 4 ms a frame.

11. Cumulonimbus

Cumulonimbus is drawn as clusters of fused towers with anvils by cloud_cumulonimbus.glsl, instead of by the legacy carve, over a cumulus field the cells draw. The model owns every cumulonimbus column from its base to 1.43 times its depth, so the carve steps aside.

  • Placement. A 45 km cube lattice in the body-fixed pattern space (section 15) holds at most one cluster per cube, placed uniformly in it and standing only within half a side of the reference sphere: 60 % of cubes raise one where the low layer’s atlas texel at its projected centre holds cumulonimbus. cb_cluster_place derives everything else from the cube’s hash: the main tower’s radius, 4.5 to 8 km, and its height, 0.55 to 1 of the layer’s depth, so neighbouring storms stand at different heights.
  • Cluster. Three shoulder towers of 0.6 the main radius stand 0.8 radii from its centre at hashed angles, 0.35 to 0.75 of its height. cumuliform_density draws each with the tower style, a side exponent of 2.5, a first billow of 0.4 of the part’s radius kept between 1200 and 3000 m (cb_billow_largest), turrets between 0.9 and 1.05 of the height and a base kept at 0.75 of the core, and overlapping parts add up to the layer’s water, so the cluster reads as one heaped mass. Each part’s sun path is cumuliform_sun_depth; the column above a sample, times its water and the 0.8 fill, dims its skylight, so a tower darkens toward its base.
  • Cumulus field. cloud_cells_layer turns a cumulonimbus column into cumulus capped CLOUD_LAYER_CB_FIELD_METERS, 2500 m, above its base, so the cells draw a cumulus field around the towers.
  • Cluster bake. cloud_cb_clusters.glsl holds the placement, cb_cluster_place. A second CloudHeapBakePass runs it once a frame for 64 by 64 lattice columns around the camera, along the lattice axis nearest the camera’s up, three float texels a column (cloud_cb_bake.comp), and a sample reads a column back from the bake, placing it live only past the bake’s edge. Below the columns, one texel per 8 by 8 block says whether any column of the block holds a cluster, and a sample whose scan covers only empty blocks reads nothing more: over a clear sky that took the march from 5.1 to 2.5 ms from the ground.
  • Reach mask. Past the bake a sample places its clusters live, up to 16 lattice columns of ten cubes each. cloud_global_cb_reach.comp writes a fifth tile into the global atlas after the layers, one texel per 4 by 4 block of layer texels: 1 where a low-layer cumulonimbus texel lies within one lattice spacing and two texels, and 1 everywhere on a row whose scan would pass 32 texels, which is the rows by the poles. cb_scan empties a live scan whose column the mask clears (cloud_global_cb_near), and a ray remembers the last live scan that placed nothing (cb_scan_known_empty), so the probe and the density do not place it twice. At 1024 by 576 the cloud pass fell from 11.0 to 4.8 ms from 400 km and from 18.0 to 6.7 ms from 20 000 km; a ground frame inside the bake is unchanged.
  • Anvil. Only a tower past 0.72 of the depth spreads one, growing to full size by 0.95. A lens at the tower’s top, 14 % of its height thick over it and thinning to its rim, reaching 1.8 to 3 tower radii across the wind, half that on a young tower, and 1.2 to 2.2 times that along it, centred 1.5 radii east, with the drift. cb_anvil_axes caps the long axis so the frayed rim stays within one lattice spacing of the centre, which the columns a sample scans always cover; an uncapped anvil was cut straight where it left them. The base-shape channel at 6 km, read two mip levels coarse, frays its rim into broad lobes and lifts or lowers its top by up to 4 % of the tower’s height. Its density is the water times 0.6, broken by the billow channel at 2.5 km down to 0.55 of that, and its sun path is the slab above the sample.
  • Probe. cloud_cumulonimbus_reach gives the march’s probe the water of any cluster whose reach and height hold a sample, so an anvil spread over columns of another genus is marched rather than hopped; without it the anvil was cut along the atlas’s coverage. The march asks for it once an iteration (cloud_march_cluster_reach), and inside the window only where the layers left the probe clear.
  • Cell ceiling. The reach tile’s g channel pools the highest any cell can stand at, its layer’s base plus 1.43 times its depth, over the same three texels as the water. The cells return at once above it, and the march’s probe ignores the reach there, so a cumulus field under a 14 km shell is not searched at every altitude.
  • Cost. Seen from the ground at 1024 by 576, a frame of the forced cumulonimbus sky costs 7.3 ms with the cell bake and the 2x2 amortized march, of which 3.2 ms is the frame without clouds; it cost 30 ms before either.
  • Open. The shaded sides carry no billow relief, the upper surface breaks into many small bright billows, and seen from the ground the anvil hides above the frame.

12. Shadows

CloudShadowMapPass (cloud_shadow_map_pass.{hpp,cpp}, cloud_shadow_map.comp) bakes the optical depth toward the sun that the genus models hold, and cloud_sun_transmittance in cloud_shadow_common.glsl turns it into the sun’s share at the ground and at every mesh.

  • Cascades. Two 768 by 768 cascades stand side by side in one image. The fine one spans 65.5 km at 85 m a texel, the coarse one 262 km at 341 m. The reader takes the fine cascade whole to 0.45 of its half-span and hands over to the coarse one by 0.95, round the centre; the coarse one fades out the same way across its own.
  • Wrapped grids. Clouds::CloudShadowPlacement (cloud_shadow_placement.{hpp,cpp}) keeps each cascade’s grid on whole texels of cloud pattern space, centred on the camera. Grid texel g lives in image texel (g + origin) mod 768, so a texel the grid still covers after a move keeps its place and its depth. SceneUniforms::cloud_shadow_cascades publishes, per cascade, texels per metre, the grid offset and the wrap; cloud_shadow_map_space.glsl holds the mapping both sides use, and the reader filters the four texels itself.
  • Trace. Each texel starts on the ground under it and reads the cloud band of the column where its sun ray crosses the shell’s middle, from either layer atlas through cloud_column_band. Samples across the band, 16 in the fine cascade and 8 in the coarse, ask the cells, cirrus, the stratiform sheets and cumulonimbus, as the march does; the legacy carve is not read. The sun’s elevation is clamped to a sine of 0.12, and below the horizon the map is clear.
  • Cover. Resolved cloud adds its full depth. Unresolved cover folds as the march folds it: a covered path and a clear one, mixed by the largest cover met. The map stores the depth whose transmittance matches, so the readers keep their live extinction.
  • Cells. In the fine cascade, where the cell bake lists no cells, they enter as their mean cover; scanning the lattice there cost 1.2 ms a frame. The coarse cascade scans for the cells its 341 m texels resolve: as mean cover it drew a flat tone around the fine cascade’s dappling, which read as a disc from 100 km.
  • Refresh. The fine cascade bakes one sixteenth of its rows a frame, the coarse one a thirty-second. A texel the grid newly covers is traced the frame it enters. The map no longer rebakes when the near window moves: that rebake took 9 to 10 ms, once every 2.6 km of travel.
  • Cost. Seen from 15 km over 50 % cumulus at 1024 by 576, the two cascades add 0.3 to 0.7 ms a frame.

13. Motion

A cloud has depth, so under camera translation it moves across the screen more slowly than the sky behind it. Two resolves need that motion, and both take it from the march’s transmittance-weighted range (cloud_depth).

  • Where a march texel stands. A march texel holds the sample of one pixel of its block, not of the block’s centre, and the pixel changes each frame. cloud_march_registration (cloud_amortization.glsl) gives the offset that lines the texture’s texel centres up with those samples, and every read cloud_taa.comp takes of the march carries it. Without it each read sat up to a resolved pixel off, a different way each frame, and a still orbit frame resolved into stair steps.
  • The cloud resolve. cloud_taa.comp reprojects every pixel’s history at the nearest marched range among the 3 by 3 march texels around it, over the sky and over any surface. The nearest range keeps a cloud’s edge moving with the cloud rather than with the sky past it. The history is read through the Catmull-Rom filter in catmull_rom.glsl, the same one taa.frag uses; three of every four pixels carry only history each frame, and a bilinear read blurred them further each frame.
  • The frame’s velocity. SkyVelocityPass (sky_velocity_pass.{hpp,cpp}, sky_velocity.frag) writes the velocity of every pixel no surface covered: reprojected at the cloud’s range where the march found cloud, at infinity elsewhere, with the frame’s jitter removed as motion_vector() removes it. The main temporal resolve, FSR and motion blur read the sky’s motion from the velocity target like any surface’s. Before it the velocity target held zero over the sky, and taa.frag reprojected sky pixels at infinity, so a translating camera dragged the clouds’ history across the screen.
  • The resolved range. cloud_taa.comp also resolves the range, in the y channel of its weight image, blended by how much cloud each side holds, and the composite reads it for aerial perspective. Read raw, the march’s range changed each frame at a quarter of the pixels, and the haze it drives flickered: from the ground under 75° N cumulus, the frame-to-frame change at the 99th percentile was 3.0 levels of 255, with 21.5 at the horizon. Resolved, it is 0.52 and 2.5.
  • The drift. The pattern turns east about the pole at the wind’s speed, so a cloud a still camera watches moves too. CloudscapeCompilePass::cloud_drift gives how far it moved at the camera since last frame, TemporalBlock.cloud_drift carries it, and cloud_previous_uv takes it off any point with a finite range. Without it the resolve clipped a moving cloud’s history every frame. At 60 times the clock, from the ground under 75° N cumulus, the mean second difference of a still camera’s 32 frames was 0.499 levels of 255; with it, 0.431. Baking every cell texel every frame brings it to 0.305, so most of the rest is the bake’s refresh.
  • Holding still. When the eye has not moved and the sky at infinity has not turned since last frame, every history sample reprojects exactly, so the resolve stops clipping the three pixels of a block the march skipped and lets a fresh sample’s feedback reach 0.985 instead of 0.97. The clip still runs on every fresh sample, and anything moving keeps the old resolve. From the ground under 75° N cumulus, a still camera’s mean second difference over 32 frames falls from 0.131 to 0.095 levels of 255, and from 0.452 to 0.340 at 60 times the clock. Flying east at 200 m/s, the difference from the old resolve decays through the move, from 2.7 levels at its first frame to 1.3 at its sixtieth, so the history a still camera left behind is what differs and the moving resolve does not.
  • The dither. The march jitters each ray’s first step by a draw from a 64 by 64 void-and-cluster texture (Textures::generate_blue_noise, built at start-up from seed 1993 and owned by CloudPass as Textures::BlueNoiseTexture), offset each frame. CloudBudget.blue_noise_index carries its heap slot, and blue_noise.glsl falls back to interleaved gradient noise when the slot is BLUE_NOISE_UNBOUND.
  • Result. Flying 1000 m/s past cumulus from 4.5 km, frame 59 of the probe’s sequence differs from a still capture at the same place by 3.4 to 3.8 levels of 255, against 4.3 to 5.0 before. SkyVelocityPass costs under 0.05 ms at 1024 by 576.

14. The tangent frame

The cloud pattern’s horizontal axes are the cloud frame’s, not the scene’s (CLOUD_TANGENT_FRAME.md). Clouds::CloudFrame (cloud_frame.{hpp,cpp}) keeps two axes tangent to the planet under the camera, body-fixed, and turns them with the camera by the smallest rotation. Its pattern position P is where the camera stands in pattern metres: it advances by the arc the camera flies and by the low deck’s wind times each step of the clock, and it starts at eye.xz + wind * t, so at the scene origin the frame is the scene’s own axes and nothing moves.

CloudscapeCompilePass owns the frame and places the windows by P: their drift, their snapped origins, their uv mapping and the bake’s camera-relative corner. camera_pattern() returns P, which is what CloudShadowPlacement centres the shadow grids on. SceneUniforms::cloud_frame_x and cloud_frame_z publish the axes; cloud_frame.glsl maps a camera-relative scene vector to their components (cloud_frame_xz) and back (cloud_frame_vector).

Every shader reads the horizontal part of a scene vector through cloud_frame_xz: the window lookups, the sun’s and the wind’s horizontal parts, the cell bake’s exit, the ground-shadow reads. Every point built from two horizontal numbers is built through cloud_frame_vector: the atlas bake’s radial, the shadow bake’s ground point, the far window’s direction. The weather field and the atmosphere nest stay addressed in camera-relative scene metres, so the bakes convert their frame corner before reading them. CloudGroundGrid stands on the frame’s plane around the camera’s ground position, P without the wind, and the scene view’s sampler carries a grid position back into the scene before it asks the terrain.

Measured with the probe over 50 % cumulus from 15 km, one place captured from 3000 and 6000 km away along the sphere (--arc) and again with the origin moved under it: before the frame, the autocorrelation’s anisotropy differed by 12 % and 26 % and its radius by 20 % and 12 %; after it, by 6.5 % and 11 %, and 0.9 % and 7 %. At the origin the captures are identical to the bit, and the march’s cost is unchanged.

15. The body pattern

Every procedural cloud shape is a function of its body-fixed position and the clock (CLOUD_BODY_PATTERN.md), so two cameras that reach one place by different paths see the same cells.

  • Pattern space. Clouds::CloudPatternSpace (cloud_pattern_space.{hpp,cpp}) turns the body frame about the pole by the drift: the cloud layer moves east as a solid body at the low deck’s wind speed on the equator. The authored wind keeps its speed and loses its direction.
  • Precision. The CPU forms the camera’s pattern position in double and publishes the rotation from scene axes, the position split into a part float holds exactly and a rest, its height over the reference sphere, and its cell and fraction in each of sixteen lattices. cloud_pattern_space.glsl reads them through camera-relative terms: a lattice read is exact, a noise read good to half a metre, and cloud_pattern_altitude to a millimetre within 30 km.
  • Lattices. The cell, cumulonimbus and clustering lattices are cubes keyed by their absolute index; a candidate stands only within half a side of the reference sphere and is projected onto it. Each cell and cluster reads its shape in its own east and north.
  • Noise. The stratiform sheets, cirrus, cirrocumulus, the aperiodic warp and the legacy carve read three-dimensional noise at the pattern position; fields flat through their layer read it on the sphere below the sample; fibres, ripples and streets run east. The east arc jumps by a parallel’s length at the pattern’s antimeridian, so the fibres crossfade the two readings over 20 km there (cloud_pattern_seam).
  • Measured. At the equator, one place reached along the sphere from 3000 and 6000 km away and re-anchored under the camera differs by 0.49 and 0.45 of 255 on average with a fixed exposure; with phase 1’s pattern it differed by 39.7. Over four views of 50 % cumulus the cloud fraction is 5 % under phase 1’s. Under 75 N cumulus at 1024 by 576 the march costs 4.2 ms from the ground, 1.7 ms from 15 km and 2.2 ms in flight, against phase 1’s 4.9, 2.3 and 2.7 ms, and the cell bake 1.0 ms.

16. Meteorological variety

The genus, species and shape of every cloud come from the air it forms in (plan).

  • Air mass. Atmosphere::PlanetAirMassMap sits beside the coverage map and shares its warp (planet_warp.hpp). Its RGBA8 channels hold boundary-layer humidity, CAPE up to 4000 J/kg, the lifted condensation level up to 4000 m and deep-layer shear up to 40 m/s. Humidity follows the coverage map (correlation 0.84 on seed 20260818), CAPE peaks in the tropics and spreads about four times either way between neighbouring regions, and shear peaks on the mid-latitude jet and around the systems’ fronts. SeededWeather generates it after the coverage map on the same worker; Environment::planet_air_mass carries it, a second PlanetMapPass uploads it, and SceneBlock.sky_view_axis.z holds its heap slot plus one. planet_air_mass.glsl reads it and returns a neutral mass while none is bound.
  • Genus and height. cloud_layer_from_map.glsl classifies each band on its own. The low base is the condensation level (150 to 3500 m); a cumulus top is 90 m per square root of CAPE above it (0.5 to 6 km) and a cumulonimbus top 9 to 14 km. Cb needs CAPE of 1000 and humidity of 0.7; St a saturated, stable column under 600 m; Ac a broken, sheared or castellanus middle layer; Cc an unstable, weakly sheared high one. The middle and high decks shift up to a quarter of their base with humidity and a column hash. The nest labels a run by its base.
  • Cells. cloud_morphology.glsl draws each cell’s species, top share, tower count, billow scale, lean along the wind, base flatness and life stage from its key and the air mass at its centre. Species and code ride the bake’s second texel as packed float bits, so bake and density decode one value. Cell radii on the low layer scale 0.75 to 1.3 with CAPE. CloudMorphology also gives altocumulus its floccus, castellanus, lenticularis and stratiformis looks. SceneBlock.sky_view_axis.w carries the cell seed, folded from the weather seed, so each sky lays out its own cells.
  • Cumulonimbus. Each storm takes 2 to 6 shoulders, lined upshear under shear, a tilt and a downshear anvil from the shear, an overshooting top from CAPE, calvus or capillatus by maturity, and mammatus under some mature anvils.
  • Sheets. cloud_variety.glsl chooses the species and variety of stratus, altostratus, nimbostratus, cirrus and cirrostratus per 45 km region: undulatus, radiatus, duplicatus, perlucidus, lacunosus, translucidus and opacus sheets, virga under thick As and Ns, and fibratus, uncinus, spissatus, intortus and vertebratus cirrus. Altostratus takes its density from a target optical depth (1.5 to 25), so it cannot fade from view.
  • Cumulonimbus gate. A low column is Cb at CAPE 1000 J/kg with humidity 0.7, or at CAPE 2500 with humidity 0.4.
  • Billows. A cell’s billow hierarchy keeps its lobes in proportion but caps the largest tier at 1200 m and adds finer tiers down to 100 m while the footprint resolves them; the surface ramp is at most 40 m. The anvil’s fray reads the noise’s Perlin channel, whose smooth contours leave no straight-edged gaps.
  • Cost. On an RTX 3080 Ti at 41 N 29 E, median of four runs: the cloud march costs 4.06 ms from the ground and 10.6 ms from orbit against 3.72 and 9.20 ms before, and the cell bake 0.52 ms from orbit against 0.88. Unbinding the air mass map removes about 1.0 ms of the orbit difference, of which 0.26 ms are the atlas reads and the rest is the taller cloud.
  • Open. Cirrocumulus stays in the cirriform path because the cell bake scans two layers. Altocumulus forms patches, not rows. Small cells still read as smooth rounded elements, and deep towers show their finest billows only up close.

17. One column per sample

  • Column. cloud_models_sample (cloud_models.glsl) resolves a sample’s three layers once through cloud_column_resolve (cloud_column.glsl) and hands the column to every genus model. Each model used to resolve its own; outside the window each resolve is a sixteen-tap B-spline.
  • Light flag. With light false the models skip the cumuliform sun, sky and overhang taps; density, coverage and the analytic slab depths are unchanged. The shadow map, which reads only density and coverage, asks without light.
  • Cost. On 2026-10-03, 1024 by 576, RTX 3080 Ti, the image unchanged from HEAD to 1 of 255: the seeded ground frame fell from 2.91 to 2.65 ms, forced cumulus from 2.24 to 2.11 ms.
  • A light clipmap was tried and removed the same day; CLOUD_MARCH_BUDGET.md §3 has the measurement.