Open Asterune
Lighting and reflections

Lighting and reflections

Trace clustered light evaluation, cached shadow maps, temporal screen-space lighting, and reflection reconstruction.

Asterune evaluates direct lighting while shading world geometry. Separate compute passes reconstruct ambient occlusion, indirect light, and reflections from the visible depth and color buffers. These paths meet in the opaque world pass and the reflection composite, before scene-wide temporal anti-aliasing.

The distinction matters when tracing an image defect. A local light contributes through a material's lighting calculation. A reflected object contributes through a ray into the screen's depth buffer. Each path has different visibility data, history, and failure conditions.

Direct light evaluation

The world fragment shader evaluates local lights before the sun and environment. The local-light loop has high register demand. Evaluating that loop first avoids keeping the later lighting intermediates live throughout it.

The base specular response combines a GGX normal distribution, correlated Smith visibility, and Schlick Fresnel. Roughness controls the distribution of microfacet normals; the Fresnel term controls reflection strength with viewing angle. The shader clamps the squared roughness used by GGX to prevent a singularity at a perfectly smooth surface.

Environment lighting uses a 64 × 64 lookup texture indexed by view angle and roughness. Its 2 channels describe the integrated specular response. The physical lighting path uses those terms to account for multiple scattering and to divide energy between diffuse and specular contributions. Metallic surfaces remove the base diffuse contribution. Transmission also reduces that contribution.

Point and spot lights use inverse-square attenuation with a finite range window. The distance term uses a minimum distance of 0.5 studs. The range window is clamp(1 - (distance / range)^4, 0, 1)^2, which brings the light smoothly to zero at its range. Spotlights multiply that result by a smooth cone transition.

Rectangular area lights use linearly transformed cosines (LTC). The shader integrates the rectangle against tabulated transforms rather than treating the rectangle as several point lights. Clearcoat, sheen, anisotropy, and subsurface terms modify the material response in the same lighting calculation.

Cluster lists and the light tree

The default local-light culling path partitions a perspective view into 16 × 9 screen tiles and 24 logarithmic depth slices. A shared count array precedes fixed-capacity lists of 128 light indices. The grid contains 3,456 clusters.

Depth boundaries follow 0.5 × (2000 / 0.5)^(slice / 24) studs. The fragment shader uses the unjittered view projection to choose a tile and depth slice. Camera jitter therefore does not move a stationary point between screen tiles solely because the temporal sampling position changed.

The renderer schedules assignment for the first 15 slices, rounding dispatches up to 64-thread workgroups. Those slices cover the 64-stud reach used by clustered lookup. Fragments beyond that reach use the light tree directly. This bounds assignment work near the camera without removing distant lights from shading.

The assignment compute shader traverses a bounding volume hierarchy (BVH) of lights. It first rejects tree nodes against the cluster's world-space bounds. For a candidate light, it tests the depth interval and the 4 side planes of the cluster frustum. Each surviving light contributes its index to the cluster list.

An overflowing cluster records 0xffffffff instead of a truncated count. The fragment shader recognizes that value and traverses the light tree for that pixel. A crowded cluster therefore increases shading work instead of silently discarding lights after the first 128.

The tree stores axis-aligned bounds and an escape index per node. A rejected node jumps directly past its subtree, so traversal needs no shader stack. CPU tree construction divides lights around the median position on the longest axis. Changing light color or intensity updates light data without rebuilding the tree when its bounds remain identical.

Both paths perform final per-surface tests. The shader rejects out-of-range point and spot lights before loading the remaining light data, then checks the surface's lighting layer mask. Cluster membership is a conservative candidate list, not a replacement for these tests.

Sun shadows and stable depth

Sun shadows use 4 layers of Depth32Float, each with its own light camera. Cascades fit successive slices of the camera frustum. Their split calculation blends logarithmic and uniform distances with a logarithmic weight of 0.75.

Each slice receives an enclosing sphere, then an orthographic light projection. The renderer quantizes the sphere radius and snaps the light-space center to shadow texels. The light basis depends on the sun direction rather than the camera's translated position. These choices stabilize the projection while the camera moves by small amounts and make stationary depth reusable.

The shader chooses the first cascade that contains the receiver with enough margin for filtering. Near the outer 10% of its usable extent, it blends into the next cascade when that cascade also contains the receiver. Shadows fade to fully lit across the final 20% of the configured shadow distance.

Shadow lookup offsets the receiver along its geometric normal and applies a depth bias. Both depend on shadow texel size and surface slope. These offsets reduce self-shadowing while keeping the bias related to the cascade's world scale.

With soft sun shadows enabled, lookup first searches 8 rotated Poisson samples for blockers. The average blocker distance determines a penumbra radius, clamped to 1 to 12 shadow texels. A 16-tap percentage-closer filter (PCF) then estimates visibility. The cheaper path uses 8 comparison taps at a fixed radius. While temporal accumulation runs, the sample rotation changes each frame so the temporal resolve can average the sampling pattern.

Stationary sun-shadow depth has a separate cache for the main view. The scene qualifies when stationary meshes contain at least 49,152 indices, a static world bundle is present, or terrain is nonempty. An eligible cascade reuses depth when its projection and stationary membership key repeat on consecutive frames. The cache draws stationary meshes, batched parts, and terrain into clean depth. It then saves only the regions that moving casters can overwrite.

On the next matching frame, the cache restores the saved regions before drawing moving casters again. Saving localized footprints avoids copying an entire cascade for a small moving character. A changed projection or membership key discards the footprints. A secondary view that replaces the shadow maps also invalidates the destination contents. This cache is disabled on Android.

Local shadow maps have a different reuse rule. Each light hashes its placement and nearby caster contents. An unchanged key skips drawing that light's shadow layers unless a refresh is forced. Point lights draw 6 cube faces; each projected light draws 1 view. Broad-phase range tests reduce the caster set before individual face-frustum tests.

Ambient occlusion and indirect light

Screen-space lighting runs after the depth prepass and before opaque shading. Its output is an Rgba16Float texture: RGB stores indirect light, and alpha stores ambient visibility. The gather runs at half resolution in each axis, with dimensions rounded up so odd image edges remain represented.

The gather reconstructs world positions from depth and derives a normal from neighboring positions. For each horizontal or vertical difference, it chooses the neighbor with the smaller position discontinuity. This reduces the chance of deriving a normal across a silhouette.

Occlusion uses angular slices around the view direction. Each slice divides its hemisphere into 32 sectors represented by a bitmask. Nearby depth samples mark the sectors that they cover. A sample contributes only within a 3-stud radius; a 1-stud thickness model extends its angular coverage behind the visible surface. The final visibility derives from the fraction of covered sectors and is clamped to 0.03 to 1.

Sampling positions vary by pixel and accumulated frame. A Hilbert index and a low-discrepancy sequence choose slice rotation and step offsets. Step distances are squared, concentrating samples near the shading point. Samples snap to full-resolution depth texel centers to avoid alternating depth choices along sloped surfaces.

When indirect gathering is active and a previous scene frame is available, newly covered sectors also gather color. The shader projects each sample's world position into the previous view, reads retained scene color, and weights it by the newly covered angular fraction and the receiving normal. Reusing only newly covered sectors prevents several depth samples from counting the same direction repeatedly. Sampled color is capped at 16 per channel.

This color comes from surfaces represented in a preceding screen image. An off-screen surface cannot contribute through this gather. Environment lighting, reflection probes, and irradiance volumes supply other spatial information.

Independent lighting history

Screen-space lighting accumulates its own history before the opaque world pass. It does not rely solely on the final scene's temporal anti-aliasing (TAA). Low-frequency lighting noise can span several pixels, so the final TAA neighborhood can contain one noisy patch and clamp its history to that patch.

The lighting accumulation first denoises the current gather with 9 taps. Packed edge weights compare neighboring linear depths and compensate for local slope. The filter reduces contributions across depth discontinuities.

The shader reconstructs the current world position and projects it with the previous camera matrix. It gathers up to 4 history texels at that location. A history texel survives only when its saved linear depth differs from the expected previous depth by at most 3%. This reprojection uses camera motion; it does not read the scene's object motion vectors.

Accepted history is clamped to the mean plus or minus 1.5 standard deviations of a 3 × 3 neighborhood of raw lighting samples. The bounds expand to include the denoised current sample. The shader then blends current light with history, using 1 / frame_count as the current-frame weight. The frame count stops at 10, leaving a minimum current-frame contribution of 10%.

An R32Uint companion texture packs linear depth and frame count as 2 half floats. The main view retains these lighting histories. Interface world views pause retention; resizing screen-space targets or resetting temporal history invalidates the stored light history.

During opaque shading, joint bilateral reconstruction expands lighting to full resolution. The shader weights 4 nearby light texels by interpolation weight and linear-depth similarity. If all weights vanish, it uses the texel with the closest depth. This preserves silhouettes more accurately than ordinary bilinear upsampling.

Reflection tracing and composition

Screen-space reflections run after opaque shading. The world shader separates the eligible surface's environment reflection from its main color, preserving that reflection as a fallback. It also writes an Rgba16Uint surface texture containing an octahedrally encoded normal, roughness, and packed RGB565 specular weight. A separate floating-point reflection target retains the fallback color and the scale used to decode that weight.

The trace compute shader reconstructs the surface position from depth. Smooth surfaces use the mirror reflection direction. When temporal resolving is enabled, rougher surfaces sample a visible GGX normal distribution, producing a different reflection ray across frames. Reflections fade between roughness 0.45 and 0.6; a surface at or above 0.6 skips tracing.

The ray marcher projects the ray into screen space and clips it to the view. It advances through linearly spaced, jittered candidates, brackets a depth crossing, refines that interval by bisection, and attempts a secant refinement. Quality settings supply the step and refinement budgets. The step count also stops at the ray's projected pixel length, with a minimum of 2 steps.

Depth tests combine nearest and bilinear depth samples at mip level 0. The farther depth defines the crossing, while the nearer depth limits penetration. This avoids treating interpolation across a depth edge as an infinitely thick surface. The reflection thickness allowance is 0.6 + 0.01 × eye_distance studs. The desktop depth texture has mip levels, but this marcher does not traverse them hierarchically.

A successful ray reads opaque scene color at the hit pixel and caps it at 32 per channel. Confidence decreases near view edges and across the roughness fade. A back-facing hit remains with reduced confidence because the screen only contains the visible side of the reflected object.

Composition gathers the traced color at the center and up to 4 axial neighbors. The neighbor radius grows with roughness. It blends the preserved environment reflection with the traced result according to confidence, then adds the result back to scene color. A missed ray therefore retains the environment estimate instead of producing a black reflection.

The composite also adds a reactive value to scene alpha. A reflection can change while the reflecting surface remains stationary, so its surface motion vector cannot describe the reflected object's motion. The reactive value tells temporal anti-aliasing to reduce retention of that part of the pixel, limiting trails behind reflected moving objects.

Probes, volumes, and rendering modes

Reflection probes provide captured cubemaps independently of the current screen's visible pixels. The shader considers up to 16 probe records and chooses the 2 strongest candidates by box influence and volume. Optional box parallax correction intersects the reflection direction with the probe box before sampling. Roughness selects the cubemap mip level; the remaining influence comes from the environment.

Irradiance volumes provide diffuse lighting from a 3D texture atlas. At each sample, the shader reads the 3 directional components selected by the normal's sign and combines them with squared normal components. Overlapping volumes blend by box influence before their combined irradiance replaces the corresponding share of ambient lighting.

Screen-space work requires a perspective projection and the renderer's screen-space path to be enabled. The High and Ultra desktop profiles enable ambient occlusion, indirect gathering, and reflection tracing. Indirect gathering additionally requires temporal accumulation. Reflections require a scene with a single sample per pixel.

The headset profile turns off screen-space indirect light, reflection tracing, and contact shadows. It limits occlusion to 1 slice and 4 steps. Its depth texture allocates only mip level 0, and occlusion still reconstructs against full-resolution depth. These choices change the work performed per eye, not the meaning of a material's lighting parameters.