Open Asterune
Lens effects and display processing

Lens effects and display processing

Trace HDR image ownership, velocity blur, bokeh depth of field, bloom, exposure adaptation, and the final display chain.

Asterune keeps scene light in floating-point high-dynamic-range (HDR) textures until tone mapping. Temporal reconstruction, motion blur, depth of field, bloom, and sun rays operate before that conversion. Image adjustments and custom effects operate afterward. The interface enters later, so the placement of an effect determines whether it changes world content, screen content, or both.

The lens chain passes a texture view named lens_output between stages. Each active stage reads the preceding image, writes a separate output, and replaces that view. A skipped stage leaves the preceding image available to the next stage. This avoids reading and writing the same texture in a full-screen pass.

HDR sources and effect order

The initial lens source is the single-sample Rgba16Float scene image. With multisample anti-aliasing (MSAA), hardware color resolve produces that image. With temporal anti-aliasing (TAA), temporal resolve writes a separate display-sized output alongside its compressed histories. That output becomes the lens source. The spatial resolution path can replace the source with its own display-sized output instead.

Neon glow enters the scene before temporal resolve. Motion blur follows reconstruction, then depth of field, then bloom and sun rays. Exposure has a separate dependency: its measurement runs after temporal resolve and before the HDR-effects pass. The schedule resolves dependencies rather than relying on the order of enum declarations.

Temporal resolveDisplay-sized output
Motion blurSingle-sample scenes
Depth of fieldHalf-resolution bokeh
Bloom and sun raysReads adapted exposure
ExposureMeters the scene HDR attachment
Tone mappingHDR to display color
Display effectsFXAAPlace effectsShader effects
InterfaceAccessibility filter
World content onlyLens and display effects run before the interface, so they do not process screen-space interface elements.

Temporal resolve precedes motion blur, depth of field, and then bloom and sun rays. Exposure runs after temporal resolve and before both bloom and tone mapping. Tone mapping converts HDR color to display color before FXAA, place effects, and shader effects. The interface draws after all of them.

A skipped pass leaves lens_output pointing at the preceding image.

Exposure meters the scene HDR attachment bound when the renderer creates or resizes its targets. It does not meter each successive lens_output texture. Bloom and tone mapping read the adapted exposure, while the bloom threshold does not contribute to the image being metered. This separates light measurement from the additional glare that the bloom stage creates.

Velocity sampling for motion blur

Motion blur gathers samples along a pixel's motion vector. It runs when the quality profile has a nonzero sample count, the resolved shutter strength is positive, and the scene is single-sampled. A multisampled scene skips this pass. Desktop profiles allocate 0, 6, 10, and 16 samples for Low, Medium, High, and Ultra, respectively; the independent MSAA gate still applies.

For geometry, the shader reads motion from the world motion texture and maps that displacement from world resolution into display coordinates. For pixels whose reverse-Z depth is zero, it derives sky motion from previous_view_projection × inverse(current_view_projection). This lets a camera rotation blur the sky even though the sky has no finite scene depth.

The renderer clamps shutter strength to 0 through 1. The shader multiplies motion by that strength, then limits its UV length to 0.08. If the resulting reach is less than half a source pixel, the shader returns the center sample. The cap bounds the filter's reach during a large camera or object movement.

For a moving pixel, the shader starts with the center color and takes the profile's additional samples along a centered interval of the motion vector. A noise value derived from pixel position and the frame modulo 64 shifts the sample positions. That shift avoids identical regular sampling at every pixel and frame. The output preserves the center alpha.

Depth controls whether another surface contributes to the blur. Reverse-Z stores a nearer surface at a larger value. If a sample is in front of the center, its weight depends on whether that foreground sample's own motion reaches back to the center's sampling position. A stationary foreground edge therefore does not receive the same weight as an edge moving across the pixel. Background samples use full weight. This is a direct velocity gather; the implementation does not build a tile-maximum velocity texture.

Focus distances and bokeh gathering

Depth of field uses 3 passes: a half-resolution prefilter, a half-resolution bokeh gather, and a full-resolution composite. The renderer creates these targets lazily when it has active focus settings. The quality profile's glow gate also controls this path. The application removes depth-of-field settings from headset views.

The prefilter stores scene RGB and a signed circle of confusion (CoC) in alpha. It reconstructs a world position from depth and the inverse view projection, then measures distance from the camera. For multisampled depth, it selects the greatest depth sample, which represents the nearest surface. An empty depth pixel receives a distant-subject value of 100,000.

Particles can supply a separate translucent-distance texture because they do not write ordinary scene depth. When the frame has written that texture, the shader compares the particle distance with the solid surface distance and uses the nearer subject. Particle distance along the view axis is converted to distance along the pixel's view ray. An in-focus particle in front of a distant sky can therefore retain its own focus distance.

The focus calculation has an in-focus band around the focus distance. Inside that band, CoC is zero. Outside it, a smoothstep ramp reaches full strength over twice the in-focus radius. Separate near and far intensities scale the result. Negative CoC represents a subject in front of focus; positive CoC represents a subject behind it. The maximum filter radius is 11 half-resolution pixels. This is an artistic focus model with a bounded radius, rather than a physical aperture-and-focal-length calculation.

The gather walks a spiral with a golden angle of approximately 2.39996 radians. Its radius increases by step / radius, distributing more samples across larger rings. The quality profile sets the step; a smaller step increases the number of samples before the radius reaches its limit.

Each sample contributes according to whether its CoC disk reaches the gather position. If a sample's signed CoC exceeds the center's, the shader caps its radius at twice the center radius. Near samples also accumulate a coverage value. The final pass mixes the sharp image with the gathered image using the larger of the center's own blur factor and the gathered near coverage. That near coverage permits a blurred foreground subject to extend over a sharp background at the composite stage.

Neon glow and brightness bloom

Neon glow redraws glow sources into a separate HDR target and spreads that light through a downsample and upsample chain. The source pass uses scene depth, so the halo starts from visible glowing geometry. The first chain level targets approximately 270 rows, using a power-of-2 divisor capped at 4. Successive levels halve the dimensions. This keeps the halo's screen reach less dependent on output resolution.

Each wider octave carries 0.8 of the preceding octave's weight. A fractional last octave changes its contribution continuously as glow size changes. The composite normalizes the sum of those weights before adding the halo to the scene. This geometry-derived glow precedes temporal reconstruction.

Bloom instead extracts bright image content after the lens effects. It uses a 13-tap downsample, then progressively smaller images and 9-tap tent upsamples. The first downsample reduces the influence of extreme bright samples with a luminance-dependent weight. A soft threshold avoids a hard transition at the selected brightness.

Threshold brightness includes manual exposure and the adapted automatic exposure. The extraction returns HDR scene values, so exposure affects which content qualifies without applying the display exposure twice. The active chain length depends on bloom size and available texture levels, with at most 7 allocated levels.

The ordinary bloom composite adds the filtered glow multiplied by intensity. The energy-conserving mode sets the extraction threshold to zero and mixes the scene toward the normalized blurred result, with intensity clamped to 0 through 1. These modes therefore have different compositing equations. Sun rays run afterward, taking 12 brightness samples toward the projected sun with a decay factor of 0.88 and a screen-distance falloff.

Histogram exposure and adaptation

Automatic exposure uses a 64-bin histogram of log luminance. A 16 × 16 compute workgroup builds a local histogram, then adds its counts to the global histogram. Luminance below 1e-5 goes into a black bin excluded from the exposure average. The remaining bins cover a log2 interval starting at −14 with a span of 20.

Metering weights the center of the image more heavily. Each sample contributes an integer weight from 1 through 4, based on an elliptical falloff. The averaging pass removes the lowest and highest 10% of the weighted population before computing mean log luminance. This reduces the effect of a small bright region or dark border on the exposure target.

The target exposure in stops is log2(0.18) − measured_log_luminance, plus a piecewise compensation curve. The curve biases very dark measurements toward darker results instead of always lifting them to middle gray. The target is clamped to the supplied minimum and maximum exposure bounds.

Two 1 × 1 R16Float textures store alternating adapted exposure values. Adaptation blends the previous value toward the target with 1 − exp(−speed × dt), where the renderer clamps dt to 0 through 0.1 s. An empty histogram preserves the preceding value. The engine marks exposure as primed after measurement; tone mapping multiplies manual exposure by 2^adapted_exposure when automatic exposure is active and primed.

The second eye of a stereo frame reuses exposure adaptation from the first eye. Adapting twice would advance the same temporal state twice for one displayed stereo frame.

Tone mapping, sharpening, and output encoding

Tone mapping clamps negative scene values, applies exposure, and evaluates the selected curve. The available curves are PBR Neutral, AgX, fitted ACES, Tony McMapface, and an uncompressed option. Tony McMapface samples an embedded 48 × 48 × 48 lookup texture after compressing the HDR coordinate into a bounded range. The final output still clamps to the display interval when the uncompressed option is selected.

After temporal or spatial reconstruction, the tone mapper also applies contrast-adaptive sharpening. It compares the center with its 4 axial neighbors, limits the sharpening lobe against the local range, and reduces that lobe for high-frequency noise. The sharpening amount is 0.8 on those paths and zero otherwise.

The shader converts the mapped result to sRGB for dithering, adds noise on the scale of an 8-bit output step, then converts back to linear values for the render target. This places the dither in encoded display space while retaining the target's color conversion behavior.

Display effects and interface ownership

The display chain alternates between 2 intermediate targets, A and B. Tone mapping writes directly to the final frame when no later display effect needs an intermediate image. Otherwise, each stage reads the current target and writes the other one. The last required stage writes to the frame.

Fast approximate anti-aliasing (FXAA) runs first. Place effects then apply their display adjustments, including image blur, tint, grading, lens distortion, vignette, and grain. A custom shader effect receives the preceding display image; it does not automatically receive the pre-tone-mapped HDR image. A depth input is supplied only when the frame's screen-space configuration produces the depth resource it uses.

Ordinary world-surface interfaces enter the scene before translucency and post-processing, so bloom and custom scene effects can affect them. World-surface interfaces with AlwaysOnTop enabled draw over the finished world image instead.

Scene-frame images are prepared before interface drawing. The renderer then adds overlays, the AlwaysOnTop surfaces, editor handles, and screen interface content. If an accessibility filter is active, interface drawing targets an intermediate view so that the final filter includes the interface. Without that filter, the interface draws into the output frame after the scene's display effects.