Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Systems19 min read

Volumetric media

On this page
On this pageFeature gatesQuality tiersDual-lobe phase + multiple scatteringVolume-local material cacheFog volumes shadow surfacesPass orderZero-cost-offPerformance notes (shipped)Cross-platformAuthoring bounded smokeInspecting the cache Back to top

Shared froxel participating-media system: global height fog and bounded smoke volumes feed the same inject → integrate → composite path, with clustered lights, soft geometry shadows, shafts, and emission — evaluated deterministically, current-frame only.

Reference scenes: src/games/example/scenes/volumetric_reference.json (global fog) and src/games/example/scenes/volumetric_media_showcase.json (bounded media — three interiors, three graph materials, no global fog at all).

Two names, on purpose. What an author places is a Fog Volume (fog_volume component, FogVolumeComponent, hi.ComponentFogVolume) — named for the thing, because "media" reads as a media player in a component list. The renderer below it keeps the standard term participating media (volumetric_media.zig, HikariMediaVolume, media_volumes binding), because that layer also serves global height fog and "fog volume" would be wrong there. The seam is the publish boundary: publishFogVolumes reads fog_volume components and writes media_volumes.

Look ownership (density, height, albedo, anisotropy, distance, noise): Visual Zones. Pipeline placement: Rendering.


Feature gates

ControlWhereEffect
features.volumetric_fogProject RenderFeaturesoff | froxel (creation-time)
quality.volumetric_fogProject RenderQualitylow | medium (default) | high
Zone / look fog fieldsVisualLookSettings.fog + volume knobsDensity, height, albedo, anisotropy, distance, noise
Scene componentfog_volumeBounded box/sphere medium; active even when global fog density is zero
Material domainmaterial.domain = "volume"Extinction, albedo, emission, anisotropy, phase-lobe/multi-scatter, and density-noise defaults
Volume graphVolume OutputDensity and Albedo compile to bytecode; the rest (incl. Second Lobe, Lobe Blend, Multi Scatter) folds to constants
Per-lightLightComponentvolumetric_scattering_intensity (default 1; 0 skips light), cast_volumetric_shadow — shown as Fog shadows (default true)

Quality tiers

PresetTileSlicesShadow samples per intersected volume
low16px324
medium8px648
high8px12816

The froxel stack is deterministic and current-frame only. Every froxel is evaluated at one fixed point, every frame, and every integrand term is an area average over the froxel: noise is band-limited to the footprint, shadow visibility is PCF-filtered over the froxel radius, local-light attenuation is floored by min(froxel_radius, 0.25 * light_radius) so the centre sample cannot sit in a lamp core. One sample of a field with no sub-froxel content is the froxel average, so there is nothing left for temporal accumulation to do.

The medium is sampled at the exact slab centre, never dithered. The density field is band-limited (edge feather floored at one froxel extent, noise octaves fading at the footprint), so a random offset within the slab would sample decorrelated values at every boundary and re-break the band limit at the scale it protects, printing froxel-scale grain.

Only directional shadow-atlas visibility is dithered, along the slab axis, by a hash of the froxel index (spatial, bit-identical every frame). It is the one integrand term that is not analytically band-limited: the 4-tap PCF quantizes the area average to five levels and slabs are 5–12× wider than the XY texels, so a hard shadow edge crossing the slabs prints a staircase. The dither and the composite kernel are a matched pair (the kernel's spatial spread never drops below 0.3). Never advance the offset per frame. Punctual lights evaluate visibility at the same slab centre as attenuation and phase, so a dithered position cannot apply lit-side visibility to fog behind a wall.

Temporal accumulation is rejected by design, twice. A stochastic in-slab walk converged by EMA is never bit-stable at rest; a deterministic sample blended against reprojected history is bit-stable but drags the medium behind every camera move. With a deterministic integrand, history is pure inertia. Do not re-propose either.

HG is evaluated per light with this camera in the same pass, so sun shafts and lamp coronas track a pan/strafe by construction. Ray integrals are current-frame (volumetric_integrate). The 3×3 composite is a spatial upsample. The only state that survives between frames is the per-surface noise-advection offset (wind).

Dual-lobe phase + multiple scattering

Three folded per-material scalars (Volume Output pins 7–9, mirrored as fog_volume actor overrides) turn the single-lobe HG into the standard film-smoke response:

  • Second Lobe (phase_second, default −0.35) + Lobe Blend (phase_blend, default 0): mix(HG(g), HG(g_second), blend) per light. One lobe forces a choice between the bright forward silver lining and the soft glow when the light is behind the camera; real dense media show both. Blend 0 is bit-identical to the old single lobe.
  • Multi Scatter (multi_scatter, default 0): Hillaire's octave approximation in the inject light loop. Octave k sees the medium thinner (self-shadow transmittance T^0.5, T^0.25) and more isotropic (g ×0.6 per octave), weighted by ms^k and normalized by the octave energy sum — light blooms a couple of metres into dense smoke instead of the medium going black in its own shadow, and the lit side keeps its authored intensity. Only the medium transmittance is raised to fractional powers; the geometric shadow-atlas term stays linear (a wall blocks every scattering order alike), and the surface fog-shadow march stays pure Beer-Lambert.

Overlapping volumes blend all three density-weighted, like anisotropy; global height fog contributes zeros so a bounded medium's lobes fade out at its own boundary. Defaults are inert: v2 .shinmaterial blobs and untouched materials render exactly as before (SMA2 v3 carries the new floats).

The froxel grid is camera-fitted, and two things about that are load-bearing.

The range must enclose the subject from where the camera actually is. Slices span near→far only; anything past the far plane is simply not sampled. A far plane derived from a subject's own size is wrong whenever the camera sits several radii back — the medium ends up entirely behind the grid, and every gate still reports success (media=1, correct grid, no fog). Derive it from camera distance + content radius.

The ray reconstruction must include the frustum centre offset. proj[2].xy carries an authored off-centre frustum (a preview pane centres its subject inside a larger surface) plus the frame's TAA jitter. hikari_froxel_world_pos inverts ndc = A·v/view_z − C, so it needs + C; without it every sample is displaced proportionally to depth and the medium renders shifted and sheared off its own geometry. The jitter half must be subtracted back out — letting it through would shift the whole field every frame, which is the temporal instability this design exists to avoid. A symmetric frustum nets exactly zero.

Depth-column skip is on when a real Hi-Z exists: inject reads a conservative pyramid-max (media.z = mip+1) plus 0.15 m slack. Sky columns stay alive. This is not a hard per-column terminator.

Integrated volumes are FIF-persistent (one pair per ring slot), not graph transients. Async inject/integrate can otherwise overwrite the texture the previous frame's composite is still sampling — the other way a whole volume trails the camera, on every scene. The pre-integration scatter volumes (phased in-scatter + extinction) are ordinary graph transients: written and consumed inside one frame.

A 3D blur before integrate smears shafts. Hard per-column depth skip prints camera-relative bands on mixed sky/geometry tiles.

Volume-local material cache

Each admitted volume owns a 32³ local-space brick with density and albedo (RGBA16F: RGB albedo, A extinction). Six independently evaluated levels, 32³ through 1³, reuse the material interpreter's noise band-limiting; queries interpolate between levels according to their local-space sample footprint. Footprints larger than the whole volume evaluate directly. Shape feathering and the runtime density envelope are applied at the receiver, not baked.

The tiled atlas uses 512 × (96 × capacity) texels; capacity rounds the selected cached-volume count up to a power of two, at most 64 (384 KiB per slot per frame-in-flight copy, up to 72 MiB per surface at capacity 64). Padding and unassigned slots are not sampled. Surface scope and frame-in-flight ownership prevent async writes from racing previous frames' lighting. All three persistent keys are declared every frame; a successful producer commits fingerprints only after final barriers. Frame abort, surface teardown/resize, and texture replacement invalidate reuse.

Static bricks rebuild only when material values, overrides or bytecode change (or a slot is reassigned/reallocated). Placement, scale, camera movement, shape feather, lighting and runtime density envelopes do not invalidate the material field. Publication assigns slots in stable actor order before BVH sorting. Changing the selected volume set can still reassign slots.

At most eight time-driven volumes are cache-admitted per surface frame. Those beyond the budget run the interpreter at the current time instead of using stale animation. This bounds animated cache construction, not total shader cost. The first admitted animated actors have stable priority; the budget is engine policy in volumetric_cache.zig, not an authoring setting. Newly admitted static volumes can require an initial full bake in each ring slot. Animated bricks reuse a slot when the supplied shader time is unchanged.

Fog volumes shadow surfaces

A fog volume darkens the geometry behind it as well as itself. Raster, RT, transparent surface lighting and froxel self-shadows share an interval-adaptive Beer-Lambert march through the bounded-media BVH. Each admitted leaf samples a camera-independent local material cache. Missing cache producers and unadmitted leaves evaluate the material directly; they are never treated as vacuum. Every consumer filters to its own footprint. These remain bounded spatial approximations, rather than an exact transport solution.

The ray traverses the BVH once, rejecting entire subtrees against the finite ray segment. Each surviving leaf clips that segment to its local box or sphere (an ellipsoid after scaling); its fixed tier budget is then distributed across only that intersection. Empty gaps consume no material samples, and thin volumes cannot fall between globally spaced taps. The transformed direction is not normalized: interval endpoints and optical-depth integration stay in world metres under rotation, non-uniform/negative scale, and parent shear.

Overlapping leaves add optical depth independently, so no volume is dropped when several overlap. The bound is 4/8/16 material evaluations per intersected leaf per admitted light (at most 64 leaves: 256/512/1024 evaluations). This is spatial interval adaptation, not an error-driven variable sample count or an unbounded refinement loop. Dense overlap can cost more than the old four-tap march; the improvement targets thin-volume coverage and empty-space work. Sampling remains deterministic and current-frame-only. Each leaf filters its material and shape to its own interval stride; quality changes can therefore change edge softness as well as detail.

Publication retains volumes intersecting the receiver frustum expanded by the 16 m shadow-march reach. It does not discard volumes past the fog-grid distance: opaque receivers can be farther away and still receive a volume shadow. The nearest-64 cap still applies to this expanded candidate set. Increasing shadow reach requires changing the shared CPU/shader contract, not just the march.

Because it multiplies whatever visibility the shadow path already produced, it is independent of how that visibility was obtained — raster cascades, ray traced occlusion, or none. Fog volumes are deliberately absent from the acceleration structure: a participating medium attenuates a ray rather than occluding it, so no traversal can express it, and an RT-shadow build would otherwise show no fog shadows at all.

The march is gated by cast_volumetric_shadow, medium-march admission, and the bounded-media count. Raster lighting skips it after fully blocked geometry visibility. RT normalization also needs it in the unoccluded baseline. Cache hits avoid material interpretation; misses pay the bounded BVH/program cost.

Evaluating the medium away from the froxel grid is what made this possible: hikari_fog_volume_density / hikari_cached_volume_sample take an explicit sample_extent and time rather than HikariFroxelParams, so any shader can evaluate the volumes. Each consumer band-limits to its own sampling rate — the froxel to its extent, the shadow march to its stride — instead of borrowing a footprint from a sampler with different spacing.

One authored toggle governs both. Fog shadows on a light controls whether it shafts through smoke and whether smoke dims it on the floor, because those are the same physical fact. There is deliberately no per-volume opt-out yet.


Pass order

PassNotes
volumetric_densityRebuild dirty local density/albedo bricks and their analytically filtered levels; retain unchanged bricks
volumetric_injectScatter into froxels; cluster light loop + per-light current-view HG + soft volume shadows; deterministic slab-centre evaluation
volumetric_integrateIntegrate current-view scattering with slab thickness measured in metres along each viewing ray
volumetric_compositescene * transmittance + inscatter, in the render domain before glass and temporal reconstruction

Density/inject/integrate use the compute queue when async compute is available, else graphics. Assembly places them after shadow depth / G-buffer / optional depth pyramid and before AO, deferred lighting, and reflections. Under RT surface shadows the atlas may still run in volumetric_only mode for volume shafts. Composite stays on graphics and writes fog opacity for the temporal resolve's motion reactivity.

Slice coordinates use view-space Z; optical depth uses physical ray distance. Each column multiplies its Z thickness by the length of the unnormalized view-depth ray, including authored off-centre projection. Without that factor, wide-angle image edges under-attenuate the scene.

Forward transparent samples the integrated volume at its own depth when volumetrics own the medium. Analytic height fog no-ops in that case.


Zero-cost-off

  1. Feature off → no compute/composite PSOs, no volume allocate, no passes in schedule, and no media publish or upload: publishFogVolumes returns before culling anything and prepareFrame skips both media buffer commits, so an authored volume costs one branch per frame. It also warns once, and the editor paints its gizmo red — a volume that can never render is otherwise indistinguishable from one that simply looks subtle.
  2. Feature on, resolved global density 0, and no visible fog volumes → same for that frame.
  3. Feature on with no bounded media at all → the froxel kernel takes a node_count == 0 early-out and returns the global fog directly, so a scene that uses only height fog pays nothing for the media path beyond one uniform branch.
  4. Any pipeline missing → whole stack drops (never partial composite on empty volume).
  5. Per-light intensity 0 → light skipped in inject loop.

Accepted residual when off: shaders still cook into the bundle (~76 KB), like SSR/AO/RT.


Performance notes (shipped)

  • No hard per-column depth termination. That optimization is discontinuous for participating media and produced camera-relative bands around narrow geometry gaps. Cost instead scales through XY tile size, slice count, and the 1080p cap.
  • Zero-extinction early-out; per-light radius/attenuation/intensity reject.
  • Bounded media use a shadow-reach-expanded CPU frustum, keep the nearest 64 candidate leaves, and traverse a balanced stackless sphere BVH in the froxel shader. Shape/noise/program work therefore runs only after broad-phase hits. Static components cache their inverse matrix; buffers stream only changed rows. Bounds enclose transformed box corners, including parent-induced shear.
  • Lit smoke self-shadows with an interval-adaptive, 16 m capped Beer-Lambert march for at most two contributing lights per froxel. Geometry visibility still comes from the shared shadow atlas for every light; the two terms multiply. The two are the strongest contributors, ranked by intensity × falloff in a cheap pre-pass over the cluster's candidates, not the first two the loop reaches (order-of-arrival selection jumps between frames on a grid rebuild).
  • The march accumulates bounded extinction only. It shares the per-volume density with the full sample but skips global fog, albedo/phase weighting and the normalising divides, none of which it reads.
  • Bounded media sample their secondary noise in local space, decorrelated by a per-record seed; a material that wants motion says so with Volume Time. (Pushing the global fog's advected world position through worldToLocal made small volumes slide faster than large ones, forever.)
  • Constant volume materials carry no program and take the direct record path. Spatial extinction graphs use at most 24 fixed-width instructions, with no shader permutation or runtime allocation.
  • Volume shadows: medium bilinear PCF (4 diagonal hardware-cmp taps), without surface receiver bias; the wider footprint prevents narrow shafts from quantizing whole froxel columns on/off. Under RT surface shadows the atlas still runs in volumetric_only mode — a ray per froxel per light is millions of traces; froxels do not share the surface RT visibility signal.
  • A volumetric_only atlas is built for the froxel grid, not for the screen. AtlasMode chooses which lights get pages; AtlasProfile (shadow_types.zig, sole constructor AtlasProfile.forMode) chooses page size and cascade reach. Fog tiles are sized to two texels per froxel column (volumetric.maxGridColumns, a property of the quality tier alone since the grid caps at 1920×1080), and cascades clamp to fog_volumetric_distance. The RT ray length is resolved before that clamp, so surface visibility keeps the authored range. faceSignature already hashes tile.size and far_plane, so a tier or fog-distance change re-rasterises on content.
  • Geometry visibility is evaluated for every contributing light with authored fog shadows and atlas pages. The cluster's two-light volumetric admission bit only limits surface-to-light medium marches; it must never bypass wall occlusion in injection. The inject pass independently keeps its two strongest medium marches. Exceeding that budget therefore does not turn an occluded lantern into an unshadowed fog light.
  • Fog shadow samples have no surface self-intersection: do not apply surface normal offsets or receiver depth bias. Those offsets can cross thin casters.
  • Composite soft upsample: deterministic 3×3 depth-aware froxel taps whose spatial width is earned by depth disagreement: a depth-flat window resolves to the single trilinear centre tap and the kernel widens only where the depths it reconciles diverge. Depth → slice sampling is half-texel corrected (texel k stores the integral to boundary k+1), so surfaces do not average in half a slice of fog from behind themselves. Every neighbour's integration depth is additionally capped at the current receiver depth: a soft depth weight is not an occlusion bound. Composite adds no independent jitter and runs before temporal reconstruction.
  • Retina cap: quality tier keeps tile density through 1080p, then aspect-preserving ceiling.
  • Scatter volumes packed as r11g11b10 RGB (phased in-scatter) + r16 scalar (extinction), both graph transients written by inject and marched by integrate the same frame.
  • Spatial density noise: world-space trilinear value noise (1–2 octaves by quality); zone wind/strength/scale/contrast. Wind is integrated into a persistent world-space offset, so blending wind between visual zones cannot reinterpret the whole scene age and jump the density field.
  • Deterministic slab-centre sampling; local-light attenuation floored by froxel size so the centre sample cannot sit in a lamp core.

Cross-platform

Metal and D3D12 share the same graph passes, froxel formats (r11g11b10 + r16), and Akari packages. Volume shadows and composite upsample are identical on both backends.

Authoring bounded smoke

Use a first-class fog_volume component. The actor transform defines its box or sphere; non-uniform scale is supported. A volume material is a specific material domain, not a flag on a surface material:

In the editor, choose Create → New Volume Material. It opens in the same graph editor as a surface material, with a Volume Output contract and a domain-filtered node palette. Its live froxel preview can switch between Sphere and Box and includes a shadow blocker so shafts remain visible while authoring. Dragging that asset into the viewport creates a spherical Fog Volume actor; alternatively add a Fog Volume component and use its domain-filtered material picker. Shape, edge fade, and per-actor overrides stay on the component. Undo/redo, debounced cooking, and live reload use the shared material workflow.

Density and Albedo may be spatial expressions; Emission, Anisotropy and the secondary noise controls stay folded constants. That split is what keeps the light loop material-agnostic — it reads one extinction and one albedo per froxel and never asks which material produced them — while the per-froxel program stays strictly bounded. Both chains share one 24-instruction budget and one register file, so a noise field feeding density and colour costs one instruction, not two.

The spatial inputs are Volume Position (local [−0.5, 0.5]³, a colour so Split Color can take out an axis), Volume Time, and five noise fields:

NodeFieldReads as
Volume Noisesingle-octave value noisesoft, even haze
Volume FBM1–4 octaves, lacunarity 2 / gain 0.5billowing smoke
Volume Ridgedthe same octaves folded through 1 − |2n − 1|wisps, steam tendrils, creases
Volume CellularWorley F1, invertedclumping — dust and ash, which value noise never does
Volume BillowPerlin-Worley: per-octave saturate((value + cellular − 1)·2)cauliflower-lobed smoke — the cloud-renderer basis; FBM's blobs carved by cell walls. Damped octaves relax to the kernel's own mean (7/24), like ridged's 1/3

Octaves is an ordinary input, not a node setting, so it can be driven like any other value; it clamps to 4 because the froxel pays for every octave at every froxel and the medium is integrated along the ray anyway. All four normalise to [0, 1] whatever the octave count, so changing octaves does not change what Density means.

Shinra validates the graph and emits the same compact program the live preview runs. Noise is band-limited to the froxel grid. A noise feature smaller than about two froxels cannot be represented and would crawl as the camera-fitted grid slides through the world. Nothing in the authored graph knows how big a froxel is, so the kernel refuses detail it cannot carry: every noise op measures its own footprint (the larger of the column's XY extent and the slab's ray length, converted through the largest inverse-transform row length into local noise cells) and fades octaves out across one octave as they approach the limit. A fully damped octave relaxes to the field's mean (0.5 for value noise and cellular, 1/3 for ridged), so distance changes detail without changing density. The shape edge obeys the same rule: the authored edge fade is floored at one froxel extent. Authoring finer noise than the grid can resolve costs nothing and shows nothing.

Noise Scale is in cells across the volume, not metres. Volume Position spans [−0.5, 0.5] whatever the actor's world size and the noise lattice cell is one unit, so the graph Scale input and the folded material noise_scale both multiply local position. A Scale below about 2 puts the entire volume inside one cell; four to eight is the usable range, and rescaling an actor does not change the number of billows. Global height fog keeps the world-space convention: fog_noise_scale there is metres per cell.

json
{
  "kind": "com.hikari.material",
  "version": "1",
  "id": "smoke",
  "material": {
    "name": "Smoke",
    "domain": "volume",
    "volume": {
      "extinction": 2.25,
      "albedo": [0.70, 0.72, 0.75],
      "anisotropy": 0.4,
      "noise_strength": 0.8,
      "noise_scale": 2.0,
      "noise_contrast": 1.5
    }
  }
}

Assign it on an actor whose archetype includes ComponentFogVolume — the engine's fog_volume core placeable, or a game archetype that lists it. empty is not one of them, so an empty actor carrying a fog_volume block gets no component and renders nothing:

json
"components": {
  "fog_volume": {
    "material": "asset://game/materials/smoke",
    "shape": "sphere",
    "edge_fade": 0.1,
    "is_static": true
  }
}

Scene fields are optional overrides of those cooked defaults. emission is also supported. Disabling features.volumetric_fog removes the shared froxel pipeline, so both global fog and bounded smoke disappear by design.

Overriding is a value, not a latch. A medium property is "overridden" while it differs from the cooked material — nothing records that a slider was once touched, and the scene document stores only the properties that actually differ. In the inspector every medium row displays the volume material's cooked value until you edit it, and the row's own reset puts it back — the same contract the render component's material params already use. Typing the material's number back has the same effect. This matters most for Density, which also decides whether the material's extinction program runs: an override means the actor wants one constant density, so the program is skipped. Latching on first edit would freeze an animated medium on a single nudge with nothing in the UI to say so.

Editing a volume material updates every actor that is still following it. An actor that overrode a property keeps its own value for that property only.

For graphs with spatial albedo and constant density, preview bytecode emits the folded authored density, including constant arithmetic chains, just as Shinra does. The Volume Output pin default applies only to an unconnected input.

Inspecting the cache

The editor's Effects toolbar button cycles effects → volume diagnostics → off. There is no engine-defined keyboard binding; games may author their own input actions. The diagnostic grid replaces the tonemap without changing fog or temporal history inputs. Its compute-written strips also use per-frame-in-flight persistent storage while graphics reads them:

RowLeftCentreRight
1Mean extinction along the camera ray, mapped as 1−exp(−σ)Integrated in-scatter, mapped as L/(1+L)Transmittance: white clear, black opaque
2Local density at z=¼Local albedo at z=½Local density at z=¾
3Cached count: green, with animated count cyanRebuilt count: amberNon-admitted/direct count: magenta

Rows 2–3 use 8×8 mini-tiles (one tile per cache slot, or one tile per counted volume in the count panels). Counts are not per-pixel hit rates: large-footprint direct evaluation is not included in the last panel. Purple means no active volumetric producer. Density slices exclude the runtime envelope and shape feather because those are deliberately not stored in the material field. The existing profiler's volumetric pass timings include cache production. The existing nearest-64 overflow warning still reports dropped candidates.

Numerical regressions run without the editor or game: see volumetric_correctness_test.swift and the invocation in tools/README.md.

PreviousVisual ZonesNext Input

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/volumetric-fog.md
On this pageFeature gatesQuality tiersDual-lobe phase + multiple scatteringVolume-local material cacheFog volumes shadow surfacesPass orderZero-cost-offPerformance notes (shipped)Cross-platformAuthoring bounded smokeInspecting the cache Back to top