Rework the volumetric cloud deck into a modelled water cloud - #76
Open
arena-ai-coding-agent[bot] wants to merge 7 commits into
Open
Rework the volumetric cloud deck into a modelled water cloud#76arena-ai-coding-agent[bot] wants to merge 7 commits into
arena-ai-coding-agent[bot] wants to merge 7 commits into
Conversation
The volumetric style was a 2D noise field extruded through a height profile and lit by single scattering. Three measurable consequences: lobes lined up vertically through the whole depth (a picture, not a cloud), an optically thick medium rendered as a silhouette with a black underside, and the interior was about eight times dimmer than a sunlit cloud top should be. Geometry: coverage stays 2D (a cloud layer is one condensing air mass) and is now one function shared by the deck and its shadow, which used to merge the weather fill differently and so disagreed in rain. Erosion moves to a 3D billow lattice sampled in blocks above the deck's own base, so the crown breaks into individual cauliflower heads and the structure stays pinned to the world instead of swimming past the camera. A weather-driven genus model picks the height profile from lanes the frame already pushes: a closed sky is a stratocumulus sheet, a scattered one is fair-weather cumulus heaps, thunderstorms are convective towers, and precipitating cloud adds absorption. Wind shear displaces the sample position by an amplitude that grows with altitude, so towers lean and crowns curl. Light: three optical-depth probes (sun on exponentially growing strides, zenith, analytic ground bounce) feed a six-order multi-scattering expansion that relaxes scattering, extinction and phase per bounce order, through a normalised three-lobe Mie-approximating phase and a powder term that lets go toward the light. Distant cloud fades into the sky rather than being deleted at the view limit. The overall gain is calibrated against the real sky (0.75/PI of the incident irradiance for an optically thick conservative deck) instead of tuned by eye. March: energy-conserving step integral, per-pixel per-frame dithered start, transmittance early exit, distance LOD on the fine erosion octave. Thickness stays the deck's BULK and height stays its base: extinction is normalised by the slab depth, so raising thickness adds volume without adding opacity. The classic style is unchanged apart from borrowing the new sun probe, and stays out of the storm absorption and aerial perspective terms. docs/realistic-volumetric-clouds.md records the model, the reference material and where every number comes from. Two test classes guard it: nine new assertions on the transport model, and RtCloudPeriodMirrorTest re-deriving the anchor wrap identity from both the shader's octave divisors and RtComposite's pushed period. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The previous commit's comments cited another shader pack by name and quoted three of its expressions and two of its parameter names while doing so. That was sloppy in both directions: it credited a redistributable implementation for techniques that were published in the literature years earlier, and it put fragments of someone else's code — however trivial — into this repository's comments under a license that reserves all rights not explicitly granted. Nothing here was ever derived from that source: no file, function, expression, identifier, constant set or asset of it exists in this repo, and the two implementations differ mechanically at every level (GLSL sampling precomputed 3D textures through uniforms, versus Slang reading WorldPush lanes and generating every field from a periodic integer hash written here, with no texture or sampler in the module at all). The sources were read, which its license explicitly permits, and the ideas are what was taken. So: the octave relaxation schedule, the powder term, the growing-stride light march, the early exit and the edge-sharpening exponent are now attributed to where they were published (Wrenninge's multi-scattering model, the Frostbite and Nubis implementations, Mie/HG practice), and the reference section of docs/realistic-volumetric-clouds.md states the provenance and licensing position explicitly instead of leaving a reader to infer it. Comment and documentation changes only — no code, no constants, no behaviour. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The deck had the right structure for a real sky but the wrong calibration: with the convection parabola peaking at 1.0, the DEFAULT coverage (0.55) made almost every cloud a slab-filling tower, so the sky read as one thick layer with a top at the slab ceiling — the "straight top that follows the thickness slider" look, from the inside. UE5's Volumetric Cloud shape stage publishes the clearest numeric statement of what cumulus and congestus mean: a fair-weather cumulus closes its dome at roughly (0.0, 0.2, 0.42, 0.6) of the layer, a developing tower at (0.0, 0.08, 0.75, 0.98). Tuned against those two gradients: * heap crown 0.70 -> 0.55 with rounding 0.30 -> 0.26, so an ordinary cumulus domes at ~55-75% of the layer instead of hugging the ceiling; * the crown fade is clamped to close AT the slab top at the latest (crownEnd = min(start + rounding, 1)). Without it a tall crown's fade ends above the slab and the deck is sliced flat by its own ceiling; * the tower lift is now a smoothstep over local coverage (0.35..0.90) instead of a linear ramp, so one sky holds low fringes, mid heaps and tall towers at once — a linear lift gives every cloud in the sky the same height; * clear-sky convection is scaled to 0.45 of the parabola (thunderstorms still carry the full range), so scattered fair weather develops to about half its layer the way the published cumulus gradient does; * base ramp 0.07 -> 0.06: a crisper condensation-level floor. Two shaping passes, both free of extra hash lookups: * the coverage field is pushed toward its own extremes before the threshold, which separates individual clouds with clean air and crisp edges instead of a sky of connected blobs; * the billow field gets the same treatment before eroding, giving the erosion Worley-like ridge-and-cell character (defined scoops with crisp boundaries) rather than a smooth wash. docs/realistic-volumetric-clouds.md records the UE5 presets in the reference list and the new numbers in the genus and erosion sections. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The volumetric deck's erosion was a billow-only FBM: rounded lobes with soft boundaries everywhere, then a smoothstep push toward the field's extremes to fake cellular character. Every write-up of the technique — Horizon Zero Dawn, Frostbite, UE5, and the Minecraft packs that followed them — erodes with the Perlin-Worley pair instead, and the cellular half is what carves the aerated, scooped silhouette a cumulus crown actually has. It was the one field this module approximated rather than computed, because the usual answer is a precomputed 3D texture and this shader has no texture binding at all. So generate it: cloudWorley3 walks the 3x3x3 neighbourhood of the sample's cell and returns the distance to the nearest feature point, each feature being its cell's centre plus a jitter of +-0.4 cells. Keeping the jitter under half a cell per axis is what makes 27 taps an EXACT F1 rather than an approximation, and unpacking all three components from one 24-bit hash per neighbour (the new cloudHash3Bits) keeps it at 27 hashes rather than 81. It stays the most expensive thing in the density, so it replaces the fine octave only, behind the existing detail LOD and tier. The remap is fitted, not guessed. Raw F1 averages 0.511 on this lattice; 2.55x - 0.81 lands the mean on exactly 0.500 with ~25% of the range on the clamps, and that clipping is the crispness. The mean matters because the SHAPE tier substitutes this octave's expected value for it when probing optical depth, so an unfitted remap would silently bias every light probe. A regression test now pins the cellular octave, the jitter bound and the remap. Periodicity is unchanged: the Worley lattice uses the same divisor and the same hash masks as the fine billow it replaces, so the anchor wrap the mirror test proves still holds for the cellular field. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Two complaints from playing it, both fair: the deck read as translucent cotton wool stuck to a ceiling, and its depth was a rectangle the thickness slider drew. Real cumulus are opaque because they are hundreds of metres deep, and how deep one is belongs to the parcel, not to a knob. Opacity first. The deck is a compressed sky a few tens of blocks deep, so it cannot buy a real cloud's optical depth (30-100 straight up) with depth; at the physical per-metre extinction a core here reached tau 1-3, which renders as see-through fluff with no shadowed underside. CLOUD_EXTINCTION 0.115 -> 0.42 puts a developed core at tau 7-15 (opaque body, dark base, silver lining at the rim) while a low-density fringe still transmits - the core/wisp split real clouds show. The core density curve now saturates at 1 instead of topping out below it, and powder went up to frame the rim. Depth second. The volumetric march now takes its slab from cloudDeckDepth - 64 blocks of sheet, 165 of heap, 210 of tower, one reading of the same coverage/weather state that picks the profile - instead of from the pushed thickness, and a zero thickness can no longer collapse it to the flat sheet. The thickness option shapes only the classic boxes, whose extrusion genuinely is it; the volumetric clouds screen swaps that row for a greyed-out explanation, mirroring what the coverage row already does in classic. Inside the slab, each parcel draws its own vertical development (a per-cloud vigour stretch of the height coordinate) and the coverage threshold wanders across the sky (CLOUD_COVERAGE_CLUSTER), so one sky holds shallow puddles, mid heaps and merging masses with clean air between them instead of one population of identical puffs. Fluffiness last: coarse billow lobes 24 -> 48 blocks (a fifth of a cloud's width, the scale real cauliflower shows), crown erosion and edge sharpening up for a crisper silhouette, base fray up for the misty underside, belly bulge and crown rounding up for rounder masses. Also commits the lang rework that had stayed uncommitted since the deck rework (style/coverage/thickness tooltips), now with the thickness tooltip restated as classic-only, in en_us and pt_br. Regression tests pin the genus depth override, the segment gate, the classic boxes keeping the slider, the vigour stretch and the clustering mask; docs follow in §1, §4 and §5.1. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The genus-depth slab was centred on the pushed slab centre, which Java computes as configured base + pushedDepth/2. Once the volumetric march stopped reading the pushed thickness, that centre stopped matching the genus slab, and the deck's FLOOR drifted with a slider the style no longer reads: with a saved high thickness the base floated tens of blocks above the altitude the height option promises - reported from game as "the clouds are too high". The march now recovers the base from the pushed centre and the PUSHED depth (exactly the Y the options screen shows), and grows the genus slab upward from it; the crossing-exclusion zone follows the new slab's own mid-plane. Classic bounds are algebraically unchanged. The shipped default height also drops 320 -> 224: with the base now exact, a little above vanilla's 192 is where a modelled deck reads right, and the old number carried an offset that was compensating for the drift. Config javadoc follows the decision: thickness is documented as the classic-only knob it became, height as the deck's exact floor. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Played from game, the deck read as vertically stretched pillars. The cause
was the crown lift: it runs to 1.0, and a cumulus core is dense by nature, so
smoothstep(0.35, 0.90, coverage) read essentially every core as "tower" and
closed its dome at the slab top - 150-block columns on a 200-block base, with
the convection term (0.45 of the parabola at default coverage) pushing the
same direction and a 165-block slab giving the columns room to grow.
Rebalanced to real cumulus proportions (height ~0.2-0.7 of width):
* crown lift capped at CLOUD_CROWN_LIFT_MAX = 0.78, window 0.45-0.95 - only
convection (storm, or a sunny day building) carries a crown past that;
* CLOUD_CROWN_START_HEAP 0.55 -> 0.48, the published gradient's mid-layer;
* CLOUD_CONVECTION_PEAK 0.45 -> 0.30, so fair weather builds instead of
erupting;
* deck headroom 165 -> 128 blocks of heap (tower 210 -> 192), so even a
full tower stays a cloud shape;
* vigour band narrowed to 1.25-0.85, biased shallower.
Fringes still stay low, cores still rise above them, storms still fill the
sky - the spread is just the sky's, not the slab's.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
volumetriccloud style is rebuilt as a physically-modelled water-cloud deck: 3D-eroded geometry, a weather-driven cloud-genus model, multi-scattered light through three optical-depth probes, and a marching scheme that is dithered, energy-conserving and early-exiting. Theclassicstyle (vanilla's boxes) is deliberately untouched apart from borrowing the new sun probe.Full write-up — the model, the reference material, where every constant comes from, the cost budget and a tuning guide — is in docs/realistic-volumetric-clouds.md.
Why
Three measurable defects, not taste:
0.75/π ≈ 0.24·E), the old interior produced~0.03·E.Plus one missing input: the deck had a single shape regardless of weather, so rain darkened it but never made it a different kind of cloud.
Geometry
CLOUD_SHAPE_DIV = 2.0→ one cell = 48 blocks, so a cloud is a few hundred blocks across, matched to the deck's depth because real cumulus are about as wide as they are tall. It is now one function with three callers (visible density, cloud shadow, flat sheet) — the density and the shadow used to merge the weather fill differently, so in rain the deck stayed at the slider's coverage while its shadow closed the sky.CloudWeather, resolved once per march):sheetfrom coverage (smoothstep(0.55, 0.92)),convectionfrom4c(1−c)·(1−sheet)and thunder,absorbingfrom rain/thunder. Readspush.weathergated onFEATURE_WEATHER_LIGHTING, so the deck only changes shape where the rest of the renderer agrees weather exists. No new slider: it is the same state that dims the sun and thickens the fog.Light
scatter *= 0.5,extinct *= 0.4,phase_g *= 0.8,powder → √powderhalfway. Converging series, no extra march, and it is what produces a bright soft interior, a lit crown and a dark-but-not-black base.g = 0.60/ silver0.88/ back−0.22, weights0.50/0.30/0.20summing to 1) so the mixture stays 4π-normalised at every order and cannot invent energy. The tight lobe is the silver lining.0.8·max(cosT, 0)) or it eats the silver lining it exists to frame.skyBehind · (1 − transmittance)with distance, so the view-limit cutoff is hidden by cloud dissolving into the sky.CLOUD_SCATTER_GAIN = 0.85is calibrated, not eyeballed — see §5.6 of the doc.Marching
Energy-conserving step integral (
S · (σ_s/σ_t) · (1 − e^{−τ}), independent of stride length, so step count refines the silhouette instead of brightening the deck) · dithered march start fromDispatchRaysIndex().xy+push.frameIndex(pixel-only dither freezes into static, frame-only bands across the screen) · early exit atT < 0.02· fine-octave LOD between 320 and 1400 blocks · two quality tiers (diffuse bounces get 3 orders, 2 sun strides, an analytic sky probe and coarse-only erosion, keeping the energy identical).Preserved because they fix reported bugs: crossing exclusion,
CLOUD_MAX_SLAB_CROSSINGShorizon cap, horizon fade, step-count scaling, and opacity applied once to the finished march as a genuine ceiling.Controls
Thickness is the deck's bulk; height is where it sits — two independent properties a flat
clouds.pngplane could not express. Extinction is normalised by slab depth (CLOUD_REFERENCE_THICKNESS), so raising thickness adds volume without adding opacity; see-through staysCloud Opacity. Within whatever depth the player sets, the weather picks the genus: storm towers fill the slab, fair-weather heaps round off below the top, an overcast sheet hugs the bottom. Config javadoc and theen_us/pt_brtooltips now say this explicitly.Cost
Roughly 2× the previous volumetric model per pixel (~40 hash lookups per full density, plus probes), which is the price of the look — texture-based cloud passes are heavier still, and buy that budget back by sampling precomputed 3D noise instead of hashing it, which this module cannot do (it may not import
world_core, so it declares no bindings). Mitigations, in order of what they save: the cheap tier on every diffuse bounce, thedensity <= 0skip that avoids all three probes in empty air (most of a scattered deck), the transmittance early exit, the distance LOD, and shape-level probes.Follow-up: calibrated against UE5's published height gradients
The structure above was right but initially mis-calibrated: with the convection parabola peaking at 1.0, the default coverage made nearly every cloud a slab-filling tower, so the sky read as one thick layer whose top sat at the slab ceiling — a straight top that follows the thickness slider, from the inside.
UE5's Volumetric Cloud shape stage publishes the clearest numeric statement of what cumulus vs congestus means — a fair-weather cumulus closes its dome at roughly
(0.0, 0.2, 0.42, 0.6)of the layer, a developing tower at(0.0, 0.08, 0.75, 0.98)— so the genus profiles are now tuned against those two gradients (commite55e2ab):0.70 → 0.55, rounding0.30 → 0.26: an ordinary cumulus domes at ~55–75% of the layer;crownEnd = min(start + rounding, 1)), otherwise a tall crown's fade ends above the slab and the deck is sliced flat by its own ceiling;smoothstep(0.35, 0.90, coverage)over local coverage instead of a linear ramp, so one sky holds low fringes, mid heaps and tall towers at once (a linear lift gives every cloud the same height);0.45of the parabola — thunderstorms still carry the full range;0.07 → 0.06for a crisper condensation-level floor;Practical note for trying it in game: vertical development is the Espessura slider (that is the "grossura" requirement), so UE5-style towers are what thickness near 100% looks like, while 30–50% gives the scattered fair-weather cumulus of the second reference image.
Follow-up: the erosion detail is now real 3D Worley (cellular) noise
The one field this module still approximated was the detail. Every implementation the technique is known for — Horizon Zero Dawn, Frostbite, UE5, and the Minecraft packs that followed them — erodes with the Perlin-Worley pair: value/billow noise for the soft rounded lobes, cellular (Worley) noise for the crisp scoops between them. The usual answer is a precomputed 3D texture, and this shader has no texture binding at all, so the previous revision faked the cellular character by pushing the billow toward its own extremes. It got the idea, not the silhouette: billow's boundaries stay soft everywhere, which is the "aerated cotton wool" read.
Commit
8b468d9generates the real field instead:cloudWorley3is a true F1 — the distance to the nearest feature point of a jittered lattice — over the 3x3x3 neighbourhood, replacing the fine octave (the coarse one stays billow, so the pair is a genuine FBM);CLOUD_WORLEY_JITTER = 0.8), the nearest feature point is provably inside that neighbourhood, so 27 taps is an exact F1 rather than an approximation;cloudHash3Bitsunpacks all three jitter components as 8-bit slices of one 24-bit hash per neighbour, so the octave costs 27 hashes rather than 81 — and it still lives only atCLOUD_DETAIL_FULL, behind the existing distance LOD, since it is the most expensive thing in the density;2.55x - 0.81lands the mean on exactly 0.500 with ~25% of the range on the clamps (that clipping is the crispness). The mean is load-bearing — the SHAPE tier substitutes this octave's expected value for it when probing optical depth, so an unfitted remap would silently bias every light probe.Periodicity is untouched: the Worley lattice uses the same divisor and the same hash masks as the fine billow it replaces, so
RtCloudPeriodMirrorTest's anchor-wrap identity still holds for the cellular field.Follow-up: fluffy, opaque, and free of the thickness slider
Played in game against real cumulus and shader-pack references, the deck still read as translucent cotton wool stuck to a ceiling, and its depth was a rectangle the thickness slider drew. Commit
b6d18e8attacks both at the root:CLOUD_EXTINCTION0.115 -> 0.42 puts a developed core at tau ~7-15 - opaque body, dark shadowed base, silver lining at the rim - while a low-density fringe still transmits: the core/wisp split real clouds show. The core density curve saturates at 1 and powder went up to frame the rim.cloudDeckDepth- 64 blocks of sheet, 165 of heap, 210 of tower, one reading of the same coverage/weather state that picks the profile - and a zero thickness can no longer collapse it to the flat sheet. The thickness option shapes only the classic boxes; the volumetric clouds screen swaps that row for a greyed-out explanation (the mirror of what coverage already does in classic).CLOUD_COVERAGE_CLUSTER) merges neighbours into big masses in one region and shrinks them to fragments with clean air in the next.CLOUD_CROWN_LIFT_MAX = 0.78(window 0.45-0.95), heap crown 0.55 -> 0.48, convection peak 0.45 -> 0.30, deck headroom 165 -> 128 blocks (tower 210 -> 192) and the vigour band narrowed to 1.25-0.85: height lands at ~0.2-0.7 of a cloud's width, the cumulus ratio, with only convection carrying a crown past the cap.The option screen, both language files, the regression tests (genus depth override, segment gate, classic keeping the slider, vigour, clustering) and the design doc follow the same decision.
One follow-up bug fix rode along (
f6cb989): the genus slab was being centred on the pushed slab centre, so once the march stopped reading the pushed thickness, the deck's FLOOR drifted with that ignored slider - a saved high thickness floated the base tens of blocks above the altitude the height option promises ("the clouds are too high", reported from game). The base is now recovered from the pushed centre and the pushed depth - exactly the Y the screen shows - and the genus slab grows upward from it; the shipped default height also drops 320 -> 224, since with the base exact there is no drift left to compensate.Provenance and licensing
No part of this PR is derived from another shader pack's source. Photon was read as a working example of a shipping real-time cloud pass — which its license explicitly permits ("examine and learn from Photon Shaders's source code") — and what was taken from it is technique, not code: no file, function, expression, identifier, constant set or asset of it appears in this repository, and the two implementations differ mechanically at every level.
WorldPushlanes, no texture or sampler of any kind in the modulecloudHash3Bits/cloudNoise3/cloudBillow3/cloudWorley3), because the deck must survive the anchor wrapA follow-up commit also re-attributes the techniques in the comments to where they were published — Wrenninge's multi-scattering octave model, the Frostbite and Horizon Zero Dawn (Nubis) implementations, Mie/HG practice, Kokhanovsky and the CALIPSO/MODIS optical-depth climatologies — instead of naming a redistributable implementation for methods that predate it.
docs/realistic-volumetric-clouds.md§3 carries the full reference list and the provenance statement.Tests
RtCloudShaderRegressionTest— 11 new tests, each naming the artefact that returns if it fails: one shared coverage field, 3D erosion anchored to the deck base (assertsposRel.yis absent from the density function), three probes through the octave expansion, thickness→bulk normalisation, both halves of the dither seed, the energy-conserving integral, aerial perspective, genus from the already-pushed lanes, the cellular detail octave being Worley with its jitter bound and fitted remap, and classic keeping its flat vanilla shading and staying out of the storm absorption. The three existing pinned assertions are unchanged.RtCloudPeriodMirrorTest(new) — re-derives the anchor wrap identity from both sides: every octave divisor must be a power of two,CLOUD_FIELD_PERIOD_BLOCKSmust equal512 · 12 · maxDivisor / scale, every octave's own repeat must divide it exactly (24576/24576/12288/3072), and the vertical lattice (1536 blocks) must not repeat inside the deepest deck RtComposite can push. This has broken twice before, both times on a non-power-of-two divisor.No Java code changes —
RtCompositeandCausticaConfigare documentation-only in this PR, since the thickness/height split was already correct on the CPU side.All 8 shader stages compile to SPIR-V locally (Slang 2026.14); CI runs the full
gradlew buildwith-warnings-as-errors.