VOXEL REFLECTIONS
1.0.0-rc.1Open the lab ↗GitHub ↗

Architecture

Separation of responsibilities

VoxelReflections owns orchestration and optional resource ownership, not voxelization mathematics. RadianceVolume is the exchange contract between a cache backend and ReflectionTracer. ReflectionMaterialBinding controls attachment and restoration without modifying the application's render loop.

Scene → SceneCollector → SceneVoxelizer → RadianceInjector
                                               ↓
                                    DirectionalMipChain
                                               ↓
                                       StaticVoxelVolume
                                               ↓
RadianceVolume interface ← VXGIVolumeAdapter ← upstream GPU cache
             ↓
       ReflectionTracer → radiance / specular / RGBA trace nodes
             ↓
    ReflectionMaterialBinding → standard/physical node material

All shader code is expressed in TSL. There are no hand-authored GLSL/WGSL strings in the implementation. The representative generated WGSL in validation evidence is a diagnostic artifact, not a separate shader implementation.

Portable cache layout

The default cache is cubic, with 16–128 cells on each axis. World position maps to (position - boundsMin) / volumeSize. Geometry is conservatively intersected with voxel AABBs using the triangle's face normal, three box axes and nine edge/axis cross products. A cell stores average material albedo, emission and normal. It is a surface cache, not a solid interior signed-distance representation.

Lighting writes linear, opacity-premultiplied outgoing diffuse radiance plus emission. The finest level is isotropic. Every coarser level has six direction blocks packed along the X dimension of a separate 3D texture: +X, -X, +Y, -Y, +Z, -Z. Each reduction averages four ray columns; within a column it composites front over back. Opposite directions therefore preserve opposite visible colors of opaque boundaries.

At sampling time the three signed axis contributions are blended with squared direction-component weights. Explicit interpolation between adjacent levels avoids depending on automatically generated 3D mipmaps. RGBA16F and explicit LOD sampling avoid requiring float32-filterable textures. The finest DDA path has a separate sampling method and does not evaluate the coarse hierarchy.

CPU memory statistics include the numeric grid and floating-point mip chain; GPU statistics describe nominal half-float radiance storage only. CPU copies of upload arrays, temporary triangle collections and driver alignment are not included in those counters. The portable path deliberately favors auditability over sparse-volume compression.

Traversal

A surface origin is offset along its world normal by a configurable number of cells. The reflected direction is reflect(-surfaceToCamera, normal). A robust slab intersection bounds traversal even for near-parallel components.

For roughness at or below sharpThreshold, DDA visits occupied cells in parametric order. Simultaneous boundary crossings advance every tied axis. DDA returns the first opaque surface cell; exact triangle intersection inside that cell is not attempted.

For other roughness values, the cone diameter is max(cellSize, 2 * distance * roughness² * coneSpread). Diameter chooses a mip level. A sample's effective opacity is 1 - (1 - opacity)^stepScale; front-to-back integration accumulates radiance and opacity until the distance budget, volume boundary, step budget or opacity threshold is reached. The fallback receives remaining transmittance. The roughness-to-cone mapping is an approximation, not a GGX importance-sampling estimator.

UV coordinates, LOD and interpolation factors are explicitly materialized before conditional mip branches. This is essential: lazy common expressions first used inside one branch can otherwise be reused with stale values in another branch when TSL builds imperative shader flow. A GPU readback regression covers this case.

The specular helper applies an analytical split-sum environment-BRDF approximation. The binding uses material accessor nodes instead of lighting-model intermediate properties, because the latter need not be initialized when an emissive expression is evaluated.

Lifecycle and updates

A static volume starts dirty. update does nothing when neither geometry nor lighting is dirty. Geometry invalidation recollects the scene, allocates a clean occupancy field, reinjects radiance and replaces texture contents. Lighting invalidation retains geometry attributes and only regenerates radiance and mips. Camera changes invalidate neither.

Geometry/material/visibility updates are explicit; no deep scene hashing occurs each frame. Calling update is synchronous for the CPU backend, so a large bake can block the main thread. Work budgets prevent runaway triangle/voxel loops. A worker or a sparse/tiled backend can implement the same interface without changing receiver shaders, but neither is included in this release.

Texture nodes remain stable as backing textures change. Owned textures and bindings are disposed; caller-owned volumes are retained unless ownsVolume is true. Existing emissive/environment nodes are restored only if the binding still owns the material's current node, avoiding overwriting subsequent application edits.

Extensions

A backend must supply linear premultiplied radiance/opacity and world-space bounds, cell size and maximum level nodes. The optional sampleBaseNode is an optimization, not a second radiance convention. Native caches with non-cubic dimensions are supported by DDA's per-axis dimensions; the portable backend always generates cubes.

Potential independent extensions include sparse pages, clipmaps, tiled GPU voxelization, directional source BRDFs, exact mesh hit refinement, and a screen-space resolve pass. None is silently simulated or advertised as implemented here. Temporal denoising is intentionally absent because the current tracer is deterministic.