Skip to content

Latest commit

 

History

History
931 lines (789 loc) · 68.6 KB

ARCHITECTURE.md

File metadata and controls

931 lines (789 loc) · 68.6 KB

Architecture

How Plague draws a frame. graph.toml is the authority: if the two disagree, the TOML is right and this file is stale.

What the pack is responsible for

Fornax owns the pipeline: it reads graph.toml, allocates the render targets, compiles the shaders and dispatches the passes in declaration order. It draws nothing of its own. How the world looks is all in this repository.

That split shapes the design. The pack cannot ask for a pass to run "sometimes": a pass either runs or it does not, decided by its enabled_if over compile options at pack build. There is no branch to take at frame time, so quality tiers are separate passes rather than if statements, and a feature that is off costs nothing because its passes are not in the graph at all.

The frame, in order

graph.toml declares 178 passes and 227 explicit targets. Consolidation passes also create array targets. They run in file order. Grouped by job:

0. Atmosphere: 4 compute passes

atmo_transmittance → atmo_multiscatter → atmo_skyview → atmo_aerial, first in the file because nothing before them needs the sky and the resolve does. The first four each write one small fixed-size table (256 × 64, 32 × 32, 192 × 108, 1088 × 32, all rgba16f): how much light gets through from a point in the air to space, what arrives there after more than one bounce, the dome as the camera sees it from its own height, and the same march stopped at each screen froxel's depth (32 × 32 froxels, 32 depth slices out to twice the render distance, plus a slice of the sky along each froxel and one of the frame's transmittance chroma). All lit by the true sun and the moon opposite it; the aerial pass adds the fog drive's mist as a shallow layer. The mappings live in shaders/include/atmo_lut.glsl, one function per writer/reader pair; a compute reader loads the tables as storage images, a fullscreen one samples them. Those four writers and targets are unconditional. The first two passes declare reuse_when_unchanged: their output depends only on density, haze, ozone, rain and thunder. Fornax skips their kernels when those inputs and resources stay the same, but still updates descriptors and keeps sync correct. A shader reload or a new target forces a rerun. Sky-view and aerial still run every frame. Sky-view has each workgroup column march its two horizon rays once and share them; the pole case and above-horizon rays are unaffected. The opaque resolve pass writes sceneHdrUnfogged. Right after it, the fullscreen resolve_hdr pass (fog_composite.fsh) reads the depth again, adds up light along the path from camera to surface, and mixes in fog, edge fade and underwater effects to make sceneHdr. Color and light loss are worked out together, in the same shader step: no separate compute pass or lookup table stands in for this. The fog pass copies its input straight through on sky pixels, when fog is off, and for debug views that don't need it. With underwater effects enabled, a submerged eye skips the air march and air sky reads that the fog dispatcher discards; the water leg's transmittance and scattered light still run, and the render edge seals on the water's closed volume instead of the sky. It shares its air, light source and shadow math with atmo_aerial through atmo_transport.glsl; atmo_transport_compute.glsl holds only the code that binds and reads compute textures. Light along the path is added up from far to near. Near the camera, the ray is split at world-grid lines every eight blocks, and the last piece is cut short at the solid surface. Each piece uses one smooth reading of air and haze, averaged from eight light-visibility checks. Each grid cell always gets the same fixed set of checks, so a longer path adds more cells rather than more checks per cell; the far sky march keeps its old, wider spacing. atmo_mist.glsl adds still mist banks placed at fixed spots in the world, turned on by Local Mist Amount. The bank grid is 32 blocks wide and 16 blocks tall, and sits inside the Fog Height range above sea level. The same bank shape also spreads out the normal morning and weather mist, so a flat weather layer does not hide the mist pockets. One flat bank still holds the 0.15 top optical-depth limit, over 32 blocks instead of 128, so mist shows up over shorter views. Local Mist Amount at zero gives back the plain, flat weather mist. Light loss and scatter use the same density. Each point needs eight lattice checks instead of four, with no extra march steps, passes, textures, or stored frames. The sky tables (planet light loss and bounced light) skip this thin local patch. Light lost through the solid surface stays right when it mixes with colored air, with no extra storage needed. The new full-size color target costs eight bytes per pixel and one extra read and write. The new fog step is checked offline; how it looks and runs in game has not been measured yet. The aerial table still feeds light-loss data to water, reflections and cave detection. fog_aerial.glsl mixes this with the sky along the ray and the fade at the render edge. Its guard against wrong sky light at cave mouths still works, and still misses a lit gap between two sheltered points. This new fog step shares no cache and reuses nothing between frames; its speed and look still need to be checked on the owner's own machine before tuning it further.

sky.glsl retains the shared palette estimates used for surface ambient, water illumination and forward particle/banner fog, plus the scattering sky's warmth and weather grading. Their controls remain live; removing the alternate dome does not retire them.

1. Geometry: 10 passes

terrain, entities, block_entities, particles, particles_translucent, banner_patterns, shadow_entities, player_mirror, player_mirror_x, player_mirror_z.

Each is a program stem under shaders/blocks/ with a .vsh and a .fsh beside each other. These run as Minecraft draws the world, and they fill the G-buffer: albedo in linear space, normals, and the labPBR material channels. Lighting is deliberately not done here: the two forward-lit arms inside terrain.fsh are the exception, and they exist because those draws cannot reach the deferred resolve.

shadow_entities writes the shadow map, which is depth-only.

player_mirror, player_mirror_x and player_mirror_z draw the player's reflection in the floor or water, in walls facing along X, and in walls facing along Z. Each writes its own half-size target (normal, colour, material), not the entities G-buffer. The engine draws the player for them (PlayerMirrorCaster) and owns those targets. They read no runtime option, like every other geometry pass, and run when SSR_QUALITY != 0.

Opaque wall receivers use their own geometric plane, translating the stored player image from the engine's selected capture plane. The shifted ray may enter the capture frustum after starting outside it. With World Reflections enabled, a bounded voxel query checks each confirmed player hit before any screen-space early return: proven opaque or alpha-tested geometry hides that body pixel; pending or unavailable geometry cannot establish a blocker. The capture remains a single view per wall axis, so body geometry clipped out of that view cannot be reconstructed by translation.

The terrain position decoder matches Fornax's fixed-point vertex codes: 2048 steps per block, offset by -8. It recovers the integer from UNORM before scaling, so adjacent section origins preserve shared edges. Encoder and decoder updates require rebuilding meshes and shaders together.

Terrain material reads use exact texels at whole mip levels inside the owning sprite. Bounds follow Fornax's integer sidecar rectangles; misaligned leading edges are excluded where neighbouring sprites can overlap during mip reduction. This keeps emission and metal codes from crossing sprite boundaries. Missing bounds use level zero; missing overflow material pages use the engine's neutral layer.

Deferred terrain motion follows the virtual POM hit on supported UV charts: the two sprite-local axes must be orthogonal and each span one world block, within vertex-encoding precision. The completed height intersection supplies the distance along the view ray. Unsupported charts, grazing clamps and incomplete marches retain polygon motion. Raster depth and light-ray origins remain on the polygon; POM changes material lookup and its reprojection, not physical geometry.

2. Screen-space occlusion and reflection

ssao_raw → ssao_blur, then hiz (a mip chain over depth), then the reflection tier: ssr_trace_fancy or ssr_trace_fast, each followed by optional world recovery and its own blur, with ssr_upsample for the half-resolution tier and ssr_prefilter building a roughness pyramid. The source-radiance producer runs before these consumers so reflected lighting uses current sources.

The AO blur evaluates a five-by-five box using nine weighted bilinear samples. Its ssaoRaw input is R8 with linear clamp sampling; changing that sampler changes the kernel. The R8 output contains only the current frame's AO.

The tier split is the pattern to notice: ssr_trace_fancy and ssr_trace_fast are two passes with mutually exclusive enabled_if guards, not one pass with a quality branch. The blur is one shader file compiled as two passes at two sizes, because every size-dependent value comes from textureSize().

Reflection history reads last frame's picture, which was drawn with a small wobble. Opaque reflections add the wobble difference back into the motion vector; water works the pixel out straight from its own last-frame clip position. Both drop history when the engine asks for a reset. Matching the current depth is only a guess: it cannot tell that the pixel, or the thing it reflected, is the same one.

Opaque SSR traces the receiver's deterministic mirror direction. Screen-pixel noise does not tilt that ray; roughness is represented by the existing reflection-lobe filtering. Rotating a stationary eye therefore does not change the ray merely by moving the receiver to another screen pixel.

3. The deferred resolve: 2 passes

resolve is where the frame is lit: it reads the G-buffer, samples the sky from the sky-view table, evaluates the sun, filters shadows, applies ambient and blocklight, composites reflections in, and lays fog over the result. It is by far the largest shader in the pack.

resolve_hdr_rt_shadow runs before resolve and applies the shared world-position handoff in shadow_handoff.glsl, followed by shadow_filter.glsl. rtShadowComposite holds direct and wide ambient visibility in red/green, the seabed caustic query in blue, and actual selected RT coverage in alpha (averaged over the direct filter taps). resolve reads input 18; input 19 remains reserved. Its RT shadow coverage debug tints selected RT cyan and raster fallback gray; brightness follows applied visibility with a display-only floor so shadowed coverage remains visible.

The graph's [ray_traced_shadows] table declares RT_SHADOWS, runtime distance_option = "u_RtShadowDistance", and blocks_per_unit = 16. The control is an integer one-to-sixteen chunks, default two (32 blocks), measured horizontally from camera to receiving point. The engine caps effective RT distance at overall Shadow Distance and publishes its square in u_ShadowMapParams.y; zero means inactive. The fixed two-block transition lies inside that boundary. Increasing coverage does not change already-covered nearby shadow queries.

RT caster selection is distinct: relevant blockers can be distant or elevated, anywhere along the light ray through the receiving volume. rtTerrainShadowDepth.r is forward light depth (miss one), and alpha is current validity. Per tap, the pack unions valid RT terrain depth with independent sunEntityShadowMapRaw depth before comparison. Invalid RT texels and distant receivers use the complete sunShadowMapRaw. Manual bilinear comparison preserves the comparison-sampler filter; it never multiplies two complete visibility fields or interpolates raw depths before comparison.

The same handoff serves primary surfaces, caustics, water shafts, reflected surfaces and fog sample positions. Direct and ambient filtering share one projection of the receiver, one noise value and one blocker search. Without ray tracing, a soft width of exactly zero checks the middle texel once and both widths use that answer; with ray tracing each tap still counts its own cover. The engine's 148.371472-texel budget bounds ray cover, not how wide a shadow may get: taps outside good ray cover read the raster map. Cloud light is separate.

The complete raster map remains available for distant receivers and incomplete RT coverage. This implementation therefore adds tracing and mesh maintenance; it does not promise zero raster cost. Offline depth fixtures verify selection and filtering, while actual caster coverage, appearance and frame time require engine tests and the owner's client session.

Glass transport

glass_photon_cache validates exact section geometry tokens and source records before reusing local photons. It reads the admitted voxel source inventory, including its shaped face bounds, instead of the atomic 256-entry analytic list. A section-centred eligibility region includes the glass view distance, local path reach and eye movement within the section. Eligible faces receive equal integer shares of 32,768 paths; each share normalizes its own Lambertian flux. The unused division remainder emits nothing. Halton points and their Cranley-Patterson shift are keyed to absolute source geometry and sample ordinal. Source order cannot change an emitter's sample set.

glass_photon_trace fills the complete local set on invalidation and retains it on subsequent frames. Records contain RGB flux, arrival direction and geometry in an integer-origin coordinate frame. Camera rotation and movement within the coverage section need no local retracing. Changed geometry, source records, publication generations or coverage rebuild the set before gathering. Lightmap-only revisions do not change optical geometry. Changing the eligible source count can change each source's finite sample quota. This is cached finite quadrature, not progressive convergence toward an unlimited path count.

Sun and moon photons use a complete world-anchored launch grid and are retraced for the current celestial direction and shadow map. Their plane stays between one and two launch radii above the eye; three radii of trace reach cover receivers one radius below it.

glass_photon_reset and glass_photon_bin index projected photon footprints in a screen-tile hierarchy, with at most four owned links per photon. Exact tile keys reject hash collisions; world-space radius and receiver-plane checks still decide membership. glass_caustic_gather evaluates each receiver at full resolution using the current material and view. Dividing by the geometric cosine preserves the BRDF's shading-normal cosine and the normal map's relief. The companion visibility record retains the last optical interface or emitting boundary. Gathered samples must reach that boundary without crossing a blocker or a different optical medium.

The gather writes local and celestial radiance separately. The material resolve fetches its own pixel from their consolidated array at appended input 23 and applies the celestial light colour. The compute gather binds albedo and material as separate G-buffer images; virtual consolidated arrays are available only to raster consumers. glass_receiver_grid.glsl selects depth texels with integer arithmetic, including at odd image sizes. Depth reconstructs the actual jittered sample. Direct lamp visibility retains RGB weights per sampled source; direct paths through glass belong to the photon estimator.

The gather shades the retained arrival directions with the current pixel's material, shading normal and view direction on every frame. It sends that result directly to consolidation, with no caustic RGB history or material-neighbourhood filter. Newly visible receivers can query the same transport data immediately. The direct-sun ownership mask also comes from the current frame. Finite photon density can leave fixed spatial sampling error.

glass_scene.glsl supplies geometric boundary traversal for light paths. Fornax's face sidecar certifies closed unions of axis-aligned model boxes and reports render layers and UV maps. Wrapped models obtain these facts from complete renderer-owned emissions, including faces culled from the visible mesh. A state's box shape may propose connected geometry, but its union's exterior must match those emitted rectangles before it receives a closure certificate. Flush internal joints need no artificial caps. A mismatched candidate falls back to model reconstruction.

When a direction contains incompatible affine maps, the sidecar can instead provide the centre and area of its largest emitted material rectangle. glass_scene.glsl selects the largest valid material sample across the model; this record supplies neither spatial UV coverage nor a closure proof. The independent geometry certificate and render-layer checks still apply, and malformed records are ignored. Missing captures or material evidence cannot certify glass. A connected pane can therefore establish its material without inventing one affine map across broad faces and caps. Plague selects dielectrics from those facts and resource materials. Refraction follows Snell's law, Fresnel selects reflected or transmitted branches, and Beer-Lambert absorption multiplies the incoming RGB by thickness-dependent transmission. Linear material colour means transmission through one block; it is a pack convention because labPBR supplies no bulk absorption channel. Smoothness controls a GGX interface. Normal-map relief does not alter the optical boundary.

Camera glass is drawn by forward terrain, which samples its authored albedo, normal and material maps and applies screen-space refraction. glass_view keeps an unchanged pre-glass HDR background at terrain's existing input 4; its alpha is always the invalid certificate sentinel, -1. Temporal accumulation reads sceneHdrComposited directly. No random optical path can suppress a raster surface. The homogeneous optical volume used for photon transport cannot represent painted surface detail or normal-map relief, so it does not replace the camera image. Camera refraction remains a screen-space approximation without thickness-dependent volume absorption.

GI additionally gathers local glass photons at its opaque secondary hit. Photon cache and trace run before gi_resolve; glass_gi_photon_reset/glass_gi_photon_bin build a separate world-cell index (512 KiB) so off-screen secondary surfaces participate. The gather uses the same surface footprint and last-leg visibility as primary shading, then applies the hit's albedo/pi once. Missing cache/index data is an unanswered GI sample; a valid empty gather is zero. The primary screen index and material response remain separate. This supplies lamp -> glass -> opaque surface -> receiver paths, using the existing photon and GI sample budgets; candidate visibility still costs optical traversal. Local photons retain their existing direct-light/shadow enablement gate.

With glass transport enabled, primary GI paths continue through certified dielectric boundaries. gi_glass_begin saves the initial hemisphere bearing and initializes throughput. Each ordered gi_glass_step/gi_glass_trace pair checks the native opaque blocker before selecting reflection or refraction at the next optical boundary. Beer absorption, radiance eta-squared and rough-surface masking accumulate along the path; gi_glass_finalize restores the exact terminal native request and hit for sun/lamp seeding. The final resolve applies this weight once and retains the initial bearing. A budget-exhausted or unavailable path holds compatible history rather than adding black. These buffer updates require an engine supporting ordered, initialized compute storage-buffer reuse. They add 100 bytes per GI sample (25 MiB at 512-square/one sample), plus one native query per allowed boundary. This is one diffuse bounce with intervening dielectric events, not multiple diffuse bounces. The optical boundaries use geometric normals and roughness, not raster normal-map relief. Celestial glass photons do not yet contribute at secondary GI hits; their active sun/moon color convention differs from GI's current true-sun convention. Secondary sun/lamp straight connections still reject glass; local transmitted lighting at the secondary surface comes from glass photons.

Forward conductors use six appended terrain texel buffers to evaluate source RGB, material response and voxel visibility at the fragment's own position. A valid query replaces the vanilla block-light contribution; missing geometry or source data retains the original lightmap. Camera dielectrics retain the full vanilla-lit material tint, including opaque painted borders. An opaque surface-light query cannot replace transmission lighting merely because its result is valid and black. This fallback preserves raster appearance; it does not supply volume absorption or refracted illumination. The conductor query has no visibility history or dynamic-entity shadows. Its buffers remain allocated while the producers are disabled because terrain has unconditional input bindings. At a nine-section window the existing grid allocation uses at least 27.90 MiB with Standard light detail, or 67.77 MiB with High, excluding CPU geometry and allocator overhead.

The same optical representation runs under voxel and hardware RT lighting. Its selected photon count, finite light domain, gather radius and optical traversal limits make this an approximation; shader compilation, CPU optical checks and GPU classifier fixtures do not establish its final client appearance, per-position capture coverage or frame cost.

4. Clouds

clouds_candidates → clouds_march_volume → clouds_merge_layers → clouds_composite, with quarter, half and three-quarter resolution variants. The compute march samples the pack's 3D shape volumes against the same sky model the dome uses, so the clouds and the light they cast agree. Global Minecraft rain and thunder strengths drive weather shape, while the camera precipitation type picks rain or snow. Each march writes seven pairs of full-float targets: premultiplied colour and first density-bearing ray distance, one pair per genus. The merge sorts these layers and writes cloudsVolumeCompute plus the contribution-weighted mean of their sampled front distances to cloudsVolumeDistance. Existing history and composite passes consume these merged outputs. Weather, cloud decks, lighting and hemisphere sampling run once per 16 × 16 workgroup. The selected deck's incident sunlight or moonlight is also shared, including its atmospheric column and the End sky override. Each invocation keeps its own view direction, noise phase and ray samples; a barrier shares the setup before any invocation can exit at the edge of the image. The dispatch has seven Z workgroup planes, one per genus. Each invocation retains only its current layer while marching, preserving the independent dither phases, convective weather fade and sample budgets. Moving sorting to a separate pass removes the seven-result private arrays from the long density loop. Every missed or disabled genus writes zero so the merge cannot read a stale layer. The fourteen intermediate images use the same scale as the merged output; separate targets avoid atlas tile rounding errors at odd viewport sizes. Their float32 storage avoids an extra half-float quantization before merging, at a transient cost of 140 bytes per cloud pixel (about 141 MiB at 1296 × 813). Only the selected resolution allocates these targets, and clouds Off allocates none.

Before marching, clouds_candidates builds conservative membership masks for seven decks in a 512 × 3585 R32F atlas. Each of the 512 × 512 deck tiles stores nine candidate bits; one extra row stores frame stamps. Ellipse/lobe bounds omit nonnegative patch and vertical penalties, retaining every candidate that could produce visible density anywhere in the tile. The march visits retained bits in the original order. An empty mask returns the sheet value and its erosion owner before evaluating unused patch noise. Rank and support checks run procedurally on cache misses; other cloud callers keep that original path. Both passes import the same frame/deck setup.

Masks encode as 1 to 512, reserving zero for cleared/unavailable data. Wrong dimensions, malformed entries or a mismatched frame stamp fall back to the original field. The frame stamp uses the engine's wrapped frame counter, not a globally unique generation identifier. The atlas is rebuilt in graph order with compute write/read synchronization and has no temporal history. Candidate culling changes work, not march resolution, sample counts, lighting, density or winning ownership. Offline GPU parity and dispatch measurements do not establish live FPS or stability in motion.

Under water the composite returns nothing at once, before it reads any source pixels. The composite step works out how far the land and water are at each screen pixel, then reconstructs cloud colour from sixteen source pixels with positive cubic B-spline weights. The separable kernel is the convolution of four unit-area boxes; it smooths the source sampling grid while preserving constant colour and opacity. Each tap reads colour and front distance from the same source pixel. A tap contributes only when it sits in front of the destination land and water. Rejected taps contribute zero without renormalizing the remaining weights, avoiding inflated opacity at terrain edges. Premultiplied colour and opacity are filtered together, then converted to the straight colour format the next pass needs. The wider kernel softens silhouettes and does not resolve temporal aliasing already present in the density march.

Cloud placement has two separate coordinate systems. The density volumes keep their fixed 57.6-block world X/Z lobe frame, shear, wind and drift. A separate unwarped 230.4-block allocation grid searches a fixed 3x3 neighbourhood of deterministically jittered sites. Each active site adds an owner-local potential made from a tall core, lower side boil and raised crown. That potential biases the cutoff of the sampled 3D density; it is never multiplied into final density, so it cannot become a visible circle, ring or Voronoi edge. Overlaps take the strongest potential without showing the Cartesian allocation cells. Cloud Amount changes only the immutable-rank activation threshold. Zero amount is exactly empty, and the dry maximum is capped below full population so the whole lattice can never become visible. Rain and snow keep that candidate membership fixed, so a weather fade cannot cross a hard rank, pop in a whole cloud, and only then grow it. They change deck depth, optical depth, horizontal footprint and profile smoothly instead; thunder may also add storm candidates while keeping the accepted full-thunder population endpoint. No weather state moves the candidate sites. Each site also owns a small stable base-height offset, so single cumulus keep locally flat bases without the whole deck sharing one plane.

Before evaluating a candidate's ellipse, lobes and vertical profile, the density query rejects sites outside a conservative support circle derived from its existing empty-density cutoff. The bound includes every lobe, ellipse stretch and floating-point slack; nearly zero orientation seeds bypass it because their normalization floor can collapse the frame. Sampling, lighting, cloud resolution and visible winning potentials are unchanged.

Cloud Size = 0.30 is the physical reference. Size changes physical depth and the isosurface bias inside each fixed owner potential; it never scales a radius, offset or noise coordinate. So the real 3D sampled field owns the growing silhouette around a stable centre. Base and detail volume coordinates keep the reference 76.8-block Y period and fixed X/Z frame, and the base blend stays 20% local to 80% organizational. The broad organization lookup uses a derived 5.3333 X/Z scale, giving it the same 307.2-block period up and sideways instead of a pancake-biased field. Rain broadens the owners into a seven-okta, low-family stratiform/congestus layer; snow picks a slightly shallower, flatter endpoint. Thunder widens the deck further and reaches the spreading-top storm family, rather than putting the calm cumulus footprint under a taller slab. Amount never reaches the per-cloud cutoff. The rain/snow cover lane also closes the cloud-lighting ambient aperture from its clear estimate toward 0.875, so the cloudy sky does not keep a mostly-sunny fill while its visible bodies overlap.

The coarse density path takes exactly two base-shape volume samples, and the full path adds two detail samples. The nine owner-site tests are ALU-only: no sampler, target, pass, history, ray step or sun tap is added. At the reference setting the dry deck resolves to 76.8 blocks, Balanced advances six slab steps, and grazing rays are capped at 64 steps. These contracts aim at detached, flat-based cumulus groups with rounded vertical crowns rather than one continuous rolling layer. The shape and the control response are subject to owner live acceptance.

Each step of the cloud ray march stays inside its own piece of the ray, even as later steps grow longer. Distance and the deck's step scale reduce its tier budget to a fixed four-sample slab floor and eight-step grazing cap floor. Those floors never rise with tier or morphology: doing so cancels the distance reduction. The low-detail mode halves the budget before these safety floors apply. The step count is always capped at the loop limit, even when rounding pushes it one step over. Sampling changes between frames only when engine temporal AA or cloud temporal smoothing is enabled. With both off, spatial dither stays fixed; movement can still reveal sampling aliasing. This uses the engine's FX_TAA compute preamble. Older engines that omit that fact retain the previous animated sequence until updated.

Cloud lighting treats the density as a medium light passes through rather than a normal-mapped surface. Each quality tier keeps its coarse direct-light fan. The fan's accumulated optical depth is extended toward the light-side slab exit with a bounded same-budget remainder estimate made from the density and fan mean already in registers. Ambient sky fill keeps its sky colour and height/coverage response. The strongly forward raw phase is mixed 10% toward an even response without losing energy, which cuts the two-octave lit forward/backward ratio from about 385:1 to 7.79:1. Powder and ambient overburden are gated by how much direct light gets through, so lit crowns stay white while shadowed interiors keep cool optical-depth separation. A bounded direct-crown exposure applies only high in the parcel when the light path is open and the camera views the light-facing crown; lower, blocked and near-sun samples keep the established response. That clear-noon-only closure fades out with rain, so its artificial +100% cap cannot punch a sunny crown through the already-overcast light palette, while the accepted clear look is unchanged. Terrain, water and LabPBR normal maps take no part in cloud transport. These add no density lookup, light tap, pass or history resource and are subject to owner live acceptance.

plagueCloudActiveDeck is the pack-owned contributor-selection seam shared by the direct compute march and the cloud-shadow query. Both read the same global rain, thunder, wetness, and camera rain/snow classification so their cloud shape agrees.

5. Water: 13 passes, the deepest part of the graph

After scene_hdr_copy snapshots the lit scene:

  • water_environment_seed → water_environment_mips build the sky water reflects. Sky only: the probe is indexed by two dot products, so a cloud written into it returns as a ring of copies around the sun. Reflected cloud comes from the screen trace, which reads the marched sky itself.
  • ssr_trace_water → ssr_blur_water trace reflections off the surface itself.
  • glint_occlusion handles sun glitter visibility.
  • water_volume_interval → water_volume_march → water_volume_scatter_history are the water column: where a ray enters and leaves the water, what it scatters along the way, and a history that keeps the result steady rather than noisy.
  • water_composite puts the surface together: reflection, refraction, foam, caustics, depth.

The water reflection ray is cut down to the part that can land on screen before any scene depth is read. A ray that starts off screen and comes back in is kept, each step is longer than the one before it, the last point before the ray leaves is sampled, and the hit search stays inside that same range. Points behind the eye or outside the depth range are thrown out.

6. Temporal: 1 pass

temporal_accumulate runs before bloom, deliberately. Accumulating a bloomed frame let a bright mover's halo hold the anti-ghost clamp open along its own path, and every star dragged a permanent comet tail. Under any anti-aliasing mode other than TAA the engine drops this to an identity copy, so the chain stays valid.

7. Underwater: 5 passes

underwater_refraction (or underwater_refraction_shafts, the same shader wired to a different output when volumetrics are on), water_volume_composite_submerged, then a separated blur: underwater_blur_h → underwater_blur_v.

The submerged shaft resolver accepts only interval-compatible half-resolution donors. When a valid full-resolution ray has none, it runs the same 8/12-cell integrator as the raw shaft pass on that ray. This covers thin surfaces and gaps absent from the half-resolution field without borrowing light across depth boundaries. A compatible donor with zero radiance remains zero. Shadow and noise inputs are appended to the resolver; its sparse integration keeps the half-resolution footprint.

8. Surface simulation: 8 compute passes

water_prepare, then water_step_a/water_step_b and water_shore in a quality or performance variant, then water_commit. This is a fluid simulation on the water surface, kept in compute because it carries state between frames. Shoreline work uses 16×16 workgroups: 32×32 groups cover the 512² Quality grid, and 16×16 groups cover the 256² Performance grid.

9. Output: 10 passes

exposure_measure reads scene luminance; bloom_blur1..7 build the pyramid and bloom_combine folds it back; tonemap maps HDR to display and applies grading. depth_copyback restores depth for anything drawn afterwards.

The exposure target stores mean luminance in r and the previous day/game clock in gba, using RGBA32F to preserve the day fraction. lighting_history.glsl distinguishes a commanded time jump from normal ticking, frozen daylight and clock rollover; a jump seeds the new measurement directly instead of adapting through the old lighting. Ordinary eye adaptation keeps its existing rates.

tonemap also owns the world outline, which is why it reads builtin.gAo, appended at input 8. The detector runs there rather than in its own pass because it must sit after temporal_accumulate (a one-pixel line is the outlier a neighbourhood clamp rejects) and needs the finished colour to composite against. tonemap is the only pass after the accumulator holding both. The outline adds proportional contrast to that finished colour, with no additive floor, so unlit surfaces stay unlit.

Where the shaders live

shaders/
├── blocks/    7 geometry stages, 14 files (.vsh + .fsh each)
├── post/      28 fullscreen passes
├── compute/   5 compute stages
├── include/   32 shared includes
└── textures/  6 pack-owned textures

Two engine rules constrain this and cannot be worked around:

  • An include must be under shaders/include/. #moj_import <fornax_runtime:x.glsl> resolves there and nowhere else. Subdirectories under it are fine: <fornax_runtime:water/waves.glsl> works.
  • A geometry program is a stem. The engine appends .vsh and .fsh, so both stages must sit together with the same name.

Options

Options are declared as annotated #defines in whichever shader is natural, and the engine's scanner merges them across every source by name. screens.toml then binds them by name onto screens; it never names a file. So an option can move between shaders freely.

Two kinds, and the difference is not cosmetic:

  • compile options are baked at pack build. They can gate a whole pass through enabled_if, so turning one off removes work from the graph entirely.
  • runtime options arrive as float uniforms in a u_PackOptions block the engine prepends to every shader. They can be dragged in game with no rebuild. A runtime boolean is therefore tested > 0.5, never against an integer.

A runtime option draws as a slider only if its name appears in screens.toml's top-level sliders list as well as on its screen; listed only on the screen, it is a cycle button.

Constants

Tables are generated, not typed. tools/derive_fog.py, derive_sky.py, derive_atmosphere.py, derive_cloud_types.py and generate_foam.py emit the numbers the shaders use, and the shaders cite them. Rerun one and you get what ships: the script is the record that the table belongs to this pack.

A constant that is not generated carries a comment saying why it has the value it has: a paper, a measurement, or the render it was tuned against.

Voxel reflection recovery

voxel_water_reflection_pass.glsl holds the shared fullscreen implementation. The Debug option PLAGUE_VOXEL_PROFILE adds five timing draws: primary trace including alpha, surface decode, direct lighting including shadows, the full reflection without segment fog, and the full reflection again. Their half-size voxelReflectionProbe scratch target is never consumed by water composition. Off removes the target and all five draws. The normal draw's shader body is unchanged by this option. Prefix outputs retain intermediate fields to prevent unused-result elimination; their timing differences include those output sinks, compiler choices and cache/order effects. Full minus no-fog estimates segment fog cost; no-fog minus direct estimates secondary reflection cost. Both are marginal draw differences, not exact timings inside the normal shader, and enabled-mode FPS is not a gameplay benchmark.

Opaque recovery uses a separate entry into the same material, lighting and fog implementation. resolve_hdr_opaque_reflection_fancy and _fast run between their SSR trace and blur, at full screen resolution. The resolve_hdr prefix is the existing engine ABI that supplies active sun, moon and terrain distance. Every eligible receiver traces world geometry; the downstream merge preserves every positive SSR hit's colour and confidence. World hits, including black blockers, carry confidence one. Epic merges recovery at blur input 6, before roughness filtering and the mip pyramid; High merges after its half-resolution SSR upsample. Only the merged reflection has temporal history: raw world history must not contaminate the current bounds used to reject stale colour. Both reflection history targets use linear sampling: TAA reprojection lands between texel centres even at full resolution, where nearest sampling biases the history donor. A miss or unavailable voxel data stays zero confidence, leaving the existing sky/enclosure fallback in charge. Recovery retains the above-water-view boundary of the shared world lighting; underwater SSR is unchanged.

The final world bounce uses direct surface lighting with coverage-weighted transmission through translucent texels. Four visible surfaces bound shading, while touching full cells with the same palette entry skip internal faces. Separate panes blend separately. Exhausted or unavailable transmission retains only the known prefix; it cannot manufacture a clear background. This is a finite approximation, without secondary refraction or recursive reflection. Equivalent glass in different section palette entries can still spend separate layers.

Opaque reflected surfaces use local sources with either local lighting mode enabled. Their direct light suppresses merged block light and evaluates the existing finite source visibility probes alongside the BRDF, because a reflected world point has no screen-space visibility history.

PLAGUE_VOXEL_REFLECTIONS defaults On under Reflections and runs voxel_water_reflection between the water SSR trace and the blur, at half size. It needs reflective water and SSR on. Screen-space geometry stays the detailed source; this fills in where SSR found nothing. With the option on, SSR flags its sky guesses with negative confidence so the voxel pass can tell sky from a real hit. The blur preserves sky confidence magnitude and smoothly maps positive geometry confidence to one at the producer's existing 0.5 tracing cutoff. This narrows the geometry fade while keeping the voxel work budget and avoiding a confidence jump where the producer stops. Fractional voxel coverage blends confidence and premultiplied colour continuously; complete coverage retains the geometry-priority handoff. Underwater bypasses this mapping and voxel recovery. The target is allocated next to water SSR so the blur's appended input binds even with the option Off, which draws and reads nothing.

A hit carries the shape crossing, face normal, local position and material entry. Mapped faces read colour, tint, normals and the labPBR maps; anything else uses the harvested average. Cutouts test alpha. The walk keeps each section summary and occupancy word until the address changes. An empty section skips ahead to its far edge, adding the same float steps as the walk so a tied crossing still lands the same way. A pending section still stops the ray. Off underwater.

Both buffers are optional and allocated only when the option is on:

  • voxelLightmap (input 11): real block and sky light per cell, block4/sky4, four cells per word, 1024 words per section, same wrapped slot and Y/Z/X order as the geometry. Full cube faces read the light outside the block; partial shapes and crossed planes read their own cell. Shape bits, not material flags, pick the side.
  • voxelFaceTexture (input 10): six seven-word records per entry, atlas UVs plus face tint. Cardinal full and partial faces use their direction's record. CROSS uses the first four records, indexed by the signs of the diagonal normal's X/Z components; its palette colour lanes stay zero. Its UVs fit the same rounded box as traversal, including rotated or mirrored texture coordinates. Each record holds RGB and flags plus six affine UV float32 values: 168 bytes per palette entry, about 75.6 MiB at eight chunks. A unique opaque boundary backing can retain its texture beneath strictly exterior cutout layers; coplanar compositions remain unsupported.

For contextual full-block models, the engine records the final emitted visible face's sprite, UVs, tint index and uniform vertex multiplier. This prevents the untransformed model's placeholder sprite from appearing in reflections. Tint still uses the section-center biome approximation. Appearance and optical boundary evidence share one palette variant, within the engine-selected 96/128/192/240-entry limit; unsupported or overflowed contextual appearance stays unavailable. Appearance capture does not establish opacity or a closed optical boundary. Partial shapes and crossed plants retain their existing mapping paths.

Material shading accepts legacy face mappings and explicit boundary mappings, including translucent boundaries. Visibility consumers retain their narrower legacy contract. UV validation and clamping use the selected partial or CROSS box; validation permits bounded float32 corner roundoff at atlas edges. Glass therefore supplies texture alpha, tint, normals and material channels to the existing finite transmission path. A successful CROSS alpha intersection is a geometry hit even though its cardinal colour lanes are zero. Unsupported plant mappings still use the existing sprite alpha approximation for traversal; they cannot invent textured reflected radiance.

surface_lighting.glsl supplies the frame light colours; main_lighting.glsl and the shared BRDF shade the bounce. Palette word15 is a block's own glow; block light landing on it is not glow. Four GGX directions give one rough second bounce. Missing or unloaded coverage never claims sky.

The primary world ray separates cell traversal from its four shaded-surface budget. Touching, face-adjacent full cells with the same palette entry skip their hidden interior interface; air gaps and corner contacts start another surface. The traversal bound comes from the window's three axis extents. Zero-alpha cells consume no shading budget. A finished opaque hit or an environment exit after transmission produces a confident answer; pending, malformed, undecodable or exhausted continuations produce no answer, rather than a confident black prefix.

Fog on the reflected ray covers water to hit only. The border comes from the engine's terrain radius around the camera, whatever the ray's length; PassParams carries that radius and the sun.

glint_occlusion_voxel traces one blocker ray inside the glitter lobe and zeroes the active lane only on a confirmed hit, so the glitter source stops showing through when on-screen blockers leave the frame. Needs the engine's glint_occlusion* PassParams routing.

PLAGUE_VOXEL_COVERAGE is a separate switch showing first-surface and behind-cutout tests from the same walk. Voxel Reach is a slider under Reflections, 1 to 16 chunks in one-chunk steps, default 4; render distance and detail caps still apply.

Limits: finite grid, stand-in leaf shapes, no vertex shading, SSAO, POM, weather layering or Nether noise, nothing past the second bounce. Cost is unmeasured; no compile check or fixture says how it looks or how fast it runs.

Source inventory diagnostic

PLAGUE_SOURCE_DIAGNOSTIC is a Debug test control, default Off. Sources marks which sections hold a light source or a supported glowing material; Freshness shows whether a section's GPU copy matches the current window and material set. Counts include hidden faces and say nothing about how much light is given off. Materials the pack cannot read stay unknown. Neither view changes normal lighting or adds light bounces.

Turning it on adds two GPU buffers, voxelSectionState and voxelSourceSummary, even with water reflections off. Each section takes 32 bytes in each buffer; the source buffer also has a 32-byte header. A compute pass, voxel_source_status, checks these records and writes a 256×256 RGBA16F status image. A final pass, voxel_source_diagnostic, draws that image over the normal scene depth. Graphics code never reads the raw buffers; Fornax's compute/graphics sync hands off the image instead. A stored alpha of zero marks an image not yet written. Off removes both passes and their targets, except section state when local coloured lighting also uses it.

At a 25-section window, this adds about 1.45 MiB of GPU memory, including the 512 KiB status image, on top of the existing voxel grid. Material data is read once, at load and section-build time; each diagnostic frame only reads the small per-section summary. The overlay, the F10 counters, the shader compile check and the offline address checks do not prove how much light a source gives off, prove the GPU output is correct, or measure cost.

Face Colours adds a bounded voxelEmitterPool buffer (64-byte header plus 4096 64-byte records) and a 256×256 RGBA16F thumbnail image, another 768 KiB plus 64 bytes. Fornax admits faces from committed section snapshots with resumable, bounded CPU enumeration; it republishes the compact pool only when changed. This is an admission subset, not a complete or nearest-source list. Deferred and unsupported face counts remain separate; lightmap-only refreshes preserve the geometry keys.

voxel_source_faces checks section ownership, geometry/storage revisions and atlas generation, then evaluates sixteen texture samples per admitted face using raw intrinsic emission, labPBR alpha and unshaded albedo/tint. Known missing material maps use the unprovided-alpha sentinel. Cutout gaps emit zero. Overflow atlas pages remain unsupported until this path can address their real textures. The samples are sparse radiance, not integrated face energy; small glowing texels can be missed. voxel_source_faces_overlay displays them in a corner panel after the other diagnostics. Grey means unused, magenta means rejected, and black means a valid non-emitting sample. No transport or normal lighting changes. Both extra passes and targets exist only in Face Colours mode. The panel enters the engine's final-image history, as other diagnostic overlays do, so it can appear in reflections.

Source colour preview

PLAGUE_SOURCE_RADIANCE, default Off under Debug, compares emitted colour before local-light transport is added. Source Colour uses unshaded texture and biome tint with intrinsic and authored emission. Compare Emission shows the existing terrain rule on the left and that candidate on the right. The existing rule includes baked shade/AO and excludes coal's authored emission; normal terrain and voxel shading retain their respective policies.

While enabled, deferred terrain stores the two colours in gAlbedo.rgb and gMaterial.rgb, using the same fixed Le/(1+Le) compression and sRGB transfer. The normal intermediate lighting is then unsuitable for display. The fullscreen pass source_radiance_preview reads exact texels from consolidatedGbuf and depth and replaces the image; sky and nonterrain classes are black. Forward surfaces and held items can still draw afterward. This uses no extra target and runs no preview pass when Off. The display encoding is diagnostic only, not a source-energy storage format.

Experimental local coloured lighting

PLAGUE_LOCAL_LIGHTING, default On under Debug, replaces vanilla placed block light globally. Missing source data and unsupported lamps do not restore the merged lightmap. Sun/sky, analytic held light and visible emission remain separate. The shared block-light curve suppresses vanilla for the voxel mode, including reflection proxies; the opaque resolve also suppresses it when traced block light is active. A source-distance fallback cannot identify an individual lamp's share and would also reintroduce dynamic held lighting alongside the analytic held light. Raw light values remain available as world data.

blocks.toml opts casting sources in with root [lighting] voxel = false and per-category lighting.voxel = true. The engine resolves membership at harvest. Unselected blocks remain occluders and retain visible self-emission. Reloading source policy rebuilds publications. Opting in a shape does not manufacture missing face mappings: currently only mapped full unit source faces are sampled, so partial lamps such as torches and lanterns remain unsupported.

The engine's voxelSourceWindow ABI2 is a sparse inventory over committed voxel sections, with per-section ranges and capacity for 4096 emitting cells. Admitted world identities persist as the eye moves; capacity overflow withholds new cells instead of dropping admitted lamps. Pending sections hide their own records while retaining admission. Unknown cells are counted separately. The header's published/overflow/unknown counts describe committed data, not unloaded world space. A source's validity is checked against its section owner, storage and geometry revision. A pending or unsupported source contributes nothing; it does not cancel other known sources.

Static source sprites on overflow pages retain their existing seven-word face mapping, with the engine's 1-based page encoded in header bits 27..28. voxel_local_sources and the face-colour diagnostic append builtin.blockAtlasPages and builtin.materialAtlasPages as array samplers; shared page sampling remaps ghost UVs to the original layer before fetching either lane. The 1x1x1 retirement fallback and missing layers are unavailable sources. Animated ghosts without a full-copy layer remain unsupported. Material alpha still distinguishes authored zero from the unprovided sentinel on every page; neither source eligibility nor emission changes with resolution.

voxel_local_sources evaluates sixteen midpoint atlas samples per eligible face and reduces them to four quarter-face radiances. voxel_local_direct integrates those area samples from the actual visible surface, using emitter cosine, inverse-square transport and the material BRDF. Candidate section ranges come from the receiver's 27 neighbouring sections. The authored finite domain is 12 blocks from each source, with a smooth taper only from 9 to 12 blocks. There is no camera-range fade and no limit of three contributing faces. Adding known lamps adds their contributions.

Traced lamp shadows and the bounce

PLAGUE_LOCAL_LIGHTING, PLAGUE_LOCAL_SHADOWS and PLAGUE_GI are independent: each may be on or off in any combination. With both direct modes enabled, RT handles nearby surfaces and voxel visibility handles distant ones. u_LocalRtDistance sets a spherical RT range in chunks, default two. The final 16 blocks blend into voxel visibility. This range selects the shadow method; each lamp keeps its own light falloff. RT alone has no camera-distance cutoff in these passes. Neither direct mode restores vanilla block light on missing source data. Bounce Light adds to the direct light in every combination, including with both direct modes off.

voxel_local_sources supplies source-face radiance for direct light, traced shadows and the bounce whenever any lighting mode is on. voxel_local_direct computes the material-weighted source sum per pixel when PLAGUE_LOCAL_LIGHTING or PLAGUE_LOCAL_SHADOWS is on. voxel_local_visibility and voxel_local_accum run after traced visibility resolves. Near pixels with a complete set of compatible, current RT donors skip the voxel emitter scan and probes; transition, distant and uncovered pixels retain voxel estimates in voxelLocalVisVoxel.

PLAGUE_LOCAL_SHADOWS runs gi_light_seed, gi_light_trace and gi_light_resolve. The seed sends 1/2/4 independent rays per active cell of a selected 256/512/768/1024-square grid. Each selects a source by its RGB material contribution and samples a point uniformly across that source face. The trace pass asks whether the segment is blocked. Each request owns a weight texel in a separate vertical sample layer. The resolve first averages the within-frame RGB estimates, then averages for up to 24 frames in giLightVisRaw, using motion vectors and surface planes to reject history from a different surface.

gi_light_upsample merges the results whenever either direct mode is on. Its 2x2 RT lookup accepts positive ages as current answers. Negative ages hold RGB history: RT-only mode can shade from them, but they cannot skip voxel work or replace voxel fallback when both modes are on. RT and voxel weights sum to one through the transition. The merged RGB estimate and an answered flag in alpha enter voxelLocalVisAccum. voxel_local_combine multiplies this estimate by the source sum and writes radiance to voxelLocalDirect.rgb, with the cloud shadow in alpha. Bounce-only mode retains vanilla direct light because the bounce chain supplies only indirect light. Direct light and both visibility backends share local_light_source.glsl: the same 4096-record inventory, receiver-neighbour section ranges, actual source boxes and material response. The separate capped analytic light list (light_list_reset, light_list_build) serves bounce. Glass photon launch reads the full source inventory directly.

PLAGUE_GI is independent of the other two and adds one bounce: gi_seed_rays (cosine hemisphere ray per cell) -> gi_trace (closest hit) -> gi_shadow_seed/gi_shadow_trace (sun ray from the hit) -> gi_lamp_seed/gi_lamp_trace (lamp ray from the hit, with the summed lamp light at the hit in giLampShade) -> gi_resolve (albedo over pi times what reaches the hit; per-cell age and luminance moments in giMomentsRaw, capped at 24 frames; the colour history is giBounceMid.history, last frame's once-spread light) -> gi_blur, gi_blur_wide, gi_blur_step4, gi_blur_step8 (five-tap à-trous at steps 1, 2, 4, 8 with the variance carried in alpha and edge stops on facing, plane distance and luminance, Schied et al. 2017). Both grids test history with gi_history_test.glsl: motion-vector reprojection, then the point seen at the old spot must lie in the cell's own plane (2 percent of distance, 0.05 block floor) and face the same way. When bilinear bounce history is empty, gi_surface_cache.glsl first checks four separate surface estimates at that same cell. gi_cache_update, immediately after gi_blur, stores the once-filtered colour, raw direction, raw moments and normal/plane in four vertically stacked one tuple per selected GI cell. These estimates survive the four TAA coverage phases without blending different leaf planes. The exact unjittered inverse camera and zero camera translation gate reuse; any view change clears the older slots and leaves ordinary motion reprojection intact. A fifth full-float metadata texel carries the lighting clock: discontinuous world time rejects every GI history path, including immediate and spatial history and unanswered holds. Slots expire after 24 unobserved frames, and a returning sample uses a compounded temporal weight for the elapsed gap. A held, unanswered ray neither increases sample count nor refreshes expiry; its negative raw moment alpha preserves the elapsed gap if it becomes immediate history before tracing resumes. More than four recurring surfaces evict the least recently observed one, including on longer AA cycles.

If no cached surface matches, bounce history can recover the nearest compatible neighbour within one cell. The borrowed estimate counts as at most one prior sample, so a different coplanar lighting region cannot donate its entire accumulated confidence. The current surface target stays truthful for filtering and traced direct visibility, which keeps its original bilinear-only test. At the default 512-square grid, the cache adds 64 MiB of ping-pong surface tuples plus its small camera metadata and one compute pass, with no additional rays; native regression and owner checks are required to establish its appearance and frame cost.

gi_trace opts into Fornax's atlas_uv_encoding = "texel_u16": hit word3 carries the exact level-zero atlas texel accepted by the ray's alpha test, tagged by flag bit13. gi_resolve uses texelFetch for that format and retains half-UV decoding for legacy hits. Normalized half UVs can round across several texels on large atlases and shade a hole or neighboring sprite instead of the accepted surface. This graph requires an engine that recognizes the encoding option.

The resolve adds kD * albedo * giBounce to the local light. Ray-query passes use the engine's tracing backend. Local shadows and GI together run four base query passes, plus sun shadow work and the configured GI dielectric continuation queries when glass transport is on.

With PLAGUE_GI_VIEW enabled, gi_debug_view displays the filtered bounce before bump shaping. Its appended 2D sampler bindings expose raw/filtered light and direction, moments/history age, surface normals, motion, and pre/post-TAA scene colour to an F10 capture selecting that pass. Those extra inputs do not change the debug image. The pass is absent with the view off; it has no layered inputs because Fornax's fullscreen readback rejects an entire pass containing one.

Full opaque cubes beside a source's forward cell clip its visible face area analytically when the receiver lies within the other tangent slab. A full cube is the engine's boxCount-0 palette entry, with neither the cutout nor the cross bit set. The clipping uses the same snapped and biased endpoints as shadow traversal. Each quarter keeps its radiance, weights by its surviving area, and traces from a point inside that area that shifts a little per pixel and per frame. The shift is interleaved gradient noise stepped by the golden-ratio fraction each frame, so the engine's temporal reconstruction settles the result over a few frames instead of leaving a fixed step at each quarter edge. This avoids whole-quarter visibility steps at certified alcove side walls without adding rays. Unknown, partial, cutout and unsupported silhouettes retain ordinary quadrature. Every surviving sample still traces the complete shadow segment.

PLAGUE_LOCAL_EMITTER_SIZE, default Quarter block, sets the size of the square that gives off light, centred on the face: the whole face (Full face), half its side (Half block) or a quarter of its side (Quarter block). The four quarters are cut from that square first and only then clipped by the aperture, so a certified side wall still clips smoothly instead of switching a whole quarter on or off. Each quarter's weight divides by the square's area, so a smaller square gives off the same total light as the full face. Only the spot the light comes from gets smaller, which lets a thin block fully shadow a face instead of hiding only a sliver of it.

Visibility follows finite voxel segments from source toward receiver, rejecting nearby source-side blockers early, with opaque blocks, partial boxes, atlas-alpha cutouts, and the engine's published nearby-body bounds. Each segment is tested against every published body's axis-aligned box, so a player, a mob or a dropped item inside a light's range casts a shadow the same way a wall does. The shadow is the plain box; which way the body faces and how it moves do not shape it. An item's box is first shrunk about its centre to half its width and depth, so it matches the drawn sprite and not the wider collision box; height stays as published. It asks whether geometry blocks the segment, independently of whether a hit has a usable reflection colour. Missing or pending segment data fails closed for that sample. Rendered opaque backing is a separate face-metadata bit from a usable atlas mapping; grass overlays therefore cannot turn their opaque cube backing transparent. Terrain carries the primitive's geometric normal in gNormal.a, encoded for the target's actual SNORM16 format; exact axis codes preserve cube faces. Normal maps affect the BRDF, not which side the shadow ray starts on. Geometry stages without this payload use their shading normal as a fallback.

The sun's highlight, and a local light's own highlight (torches, lamps and other block light, in shaders/include/voxel_local_light.glsl), can also compare that geometric normal to the bumped normal, through shaders/include/microfacet_normal_map.glsl. A steep bump is treated as one side of a small V-shaped groove whose average slope matches the true surface. The code checks both the side the bump map gives and the unseen far side of the groove, so a bump facing away from a light stops sending back a highlight it could not really have, and one facing toward the light picks up extra shading from that far side. Method from Schussler, Heitz, Hanika, Dachsbacher, "Microfacet-based Normal Mapping for Robust Monte Carlo Path Tracing", ACM Transactions on Graphics 36(6), 2017. If no geometric normal is stored, the groove flattens to one side and the result matches the plain, single-sided highlight. Reflections (voxel_surface.glsl and the voxel_water_reflection*/ voxel_opaque_reflection.fsh passes) do not use this option yet and always show that plain, single-sided highlight.

Grass and foliage use actual visible points rather than a cube-face receiver cache. Known thin cutouts with labPBR subsurface response split diffuse energy between reflection and transmission, up to half in each hemisphere. This is an authored thin-sheet approximation, not volume scattering; opaque-backed grass faces do not transmit. Cutout occluders use the harvested model's approximation: two crossed planes for CROSS, the unit cube for a full cutout cell, and each stored box's own faces for a partial cutout (a door, trapdoor, pane or iron bars). The sprite rect is stretched across that box, and a box thinner than a quarter block reads as solid. Outside the certified aperture case, four area samples can leave visible steps in a penumbra; the per-quarter shift turns those steps into noise that temporal reconstruction settles over a few frames. Quarter radiance and shifted-sample integration do not resolve arbitrarily small emissive details or exact specular area-light response.

Primary lighting has a separate HDR target so its cost and output can be measured. RGB holds local radiance; alpha carries the cloud-shadow mask. Resolve reads both through its existing input 15, keeping the Metal sampler budget the same. With local lighting Off, voxel_local_off copies only the cloud mask into the same target. The source producer stops only when GI and glass transport are also off. Secondary voxel surfaces use the same direct-light transport, without a primary screen cache. This is direct lighting, not bounced GI; reflected hits have no per-hit fluid classification and the local transport does not integrate water absorption along a segment.

Visible emission and received source radiance share the scale derived by tools/derive_local_emission.py: a white unit face one block from a neutral rough wall matches the legacy block-14 reference luminance at the documented fixed settings. This establishes relative scene units, not measured lumens. Voxel and traced local lighting use this same scale; disabling both keeps the legacy emission scale.

The radiance buffer occupies 1,991,496 bytes and remains allocated for fixed bindings while Off. The engine sparse source inventory adds 418,632 bytes while enabled. Section state adds 32 bytes per voxel section when not already allocated. The full-resolution RGBA16F direct target consumes 8 bytes per pixel. Geometry buffers are shared with reflection tracing. Fornax synchronizes compute writes before graphics reads, and previous graphics reads before subsequent compute writes. Native shader fixtures verify transport and ABI cases; they do not establish the installed resource pack's appearance or live Vulkan frame cost. Owner validation remains required.

Category quality budgets

The Ray Tracing page exposes independent MetaSpec presets for GI, block light, sun shadows, reflections and glass lighting. They stage the underlying controls atomically; granular edits show Custom and existing saved settings remain unchanged. Artistic strengths, materials, normal maps and feature enable switches are not assigned by these presets.

GI and traced block light each have independent PLAGUE_*_GRID (256/512/768/1024) and PLAGUE_*_SAMPLES (1/2/4) compile options. The default is the existing 512-square, one-sample budget. Low/Medium/High/Ultra select 256/1, 512/1, 768/2 and 1024/4: 0.25/1/4.5/16 times the default base sample count. GI presets also select 2/4/8/16 dielectric interfaces through PLAGUE_GI_GLASS_INTERFACES; an isolated slab normally needs two. Every GI sample owns a bounce, sun and lamp query plus one continuation query per allowed interface when glass transport is on. Inactive paths submit disabled requests in the remaining stages. Local lighting owns one source visibility query per sample. These are explicit work budgets, not measured FPS tiers.

graph.toml uses compile-option integer expressions for all related image dimensions, request and answer buffers, ray counts, and dispatch dimensions. GI cache height remains four times the chosen side; its commit dispatch follows the receiver grid. giLocalSurfaceRaw and gi_local_surface provide a separate direct-light geometry history, preventing different grid sizes from sharing incompatible reprojected cells. Allocation changes rebuild and clear histories together.

rt_sampling.glsl interleaves records as cell * samples + sample. GI stratifies the cosine hemisphere by equal disc area; local lighting independently selects sources and face points. Within-frame linear estimates are averaged before the existing temporal/spatial filters, and GI bearings are luminance-weighted. Any unavailable required query holds compatible history for the whole estimate. This includes requested GI sun/lamp child queries; an inactive child remains a valid zero contribution. One-sample sequences retain their previous layout and random draws.

Glass lighting offers 16384/32768/65536 paths. Half go to local emitters, half to a complete sun grid (64-square with two samples, 128-square with one, or 128-square with two). Flux normalization uses actual source quotas, and the budget participates in the cache tag. Allocations, hash reset and full-resolution gathering remain at maximum; reducing paths reduces tracing/binning work, not all glass costs. Reflections use existing resolution/march controls. Primary sun shadows stay one deterministic centre ray per pixel; those presets change coverage and raster fallback cost.

The engine material capacity is independent of these sampling budgets and takes effect on restart. voxel_palette_layout.glsl derives palette/face strides from bound buffer extents and rejects inconsistent capacities. More entries retain more contextual surface facts, but do not establish that a forward normal/refraction problem was caused by palette overflow.