How Plague draws a frame. graph.toml is the authority: if the two disagree, the TOML is right and
this file is stale.
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.
graph.toml declares 178 passes and 227 explicit targets. Consolidation passes also create
array targets. They run in file order. Grouped by job:
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.
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.
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.
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_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.
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.
After scene_hdr_copy snapshots the lit scene:
water_environment_seed→water_environment_mipsbuild 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_watertrace reflections off the surface itself.glint_occlusionhandles sun glitter visibility.water_volume_interval→water_volume_march→water_volume_scatter_historyare 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_compositeputs 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.
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.
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.
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.
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.
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
.vshand.fsh, so both stages must sit together with the same name.
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:
compileoptions are baked at pack build. They can gate a whole pass throughenabled_if, so turning one off removes work from the graph entirely.runtimeoptions arrive asfloatuniforms in au_PackOptionsblock 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.
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_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.
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.
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.
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.
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.
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.