Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Post-Processing

Flint includes an HDR post-processing pipeline that transforms the raw scene render into polished final output with bloom, SSAO, fog, volumetric lighting, tonemapping, and vignette effects.

How It Works

Instead of rendering directly to the screen, the scene is drawn to an intermediate HDR buffer (Rgba16Float format) that can store values brighter than 1.0. A series of fullscreen passes then process this buffer:

Scene render         SSAO        Volumetric    Bloom chain       Composite pass
(PBR, skinned,   →  depth-based  shadow-based  downsample    →   exposure
 billboard,         AO texture   god rays      upsample          ACES tonemapping
 particles,                                                      gamma correction
 skybox)                                                         fog + vignette
     ↓                  ↓            ↓              ↓                ↓
 Rgba16Float       AO texture   vol texture    bloom texture    sRGB surface
 HDR buffer        (darkening)  (light shafts) (bright halos)  (final output)

All scene pipelines — PBR, skinned mesh, billboard sprite, particle, and skybox — render to the HDR buffer when post-processing is active. The PBR shader’s built-in tonemapping is automatically disabled so it outputs linear HDR values for the composite pass to process.

Bloom

Bloom creates the soft glow around bright light sources — emissive materials, fire particles, bright specular highlights. The implementation uses the technique from Call of Duty: Advanced Warfare:

  1. Threshold — pixels brighter than bloom_threshold are extracted
  2. Downsample — a 5-level mip chain progressively halves the resolution using a 13-tap filter
  3. Upsample — each mip level is upsampled with a 9-tap tent filter and additively blended back up the chain
  4. Composite — the final bloom texture is mixed into the scene at bloom_intensity strength

Bloom enabled — bright sources produce soft halos

Post-processing enabled: bloom creates halos around emissive surfaces and bright lights.

Bloom disabled — same scene with raw PBR output

Post-processing disabled: the same scene rendered with shader-level tonemapping only.

The mip chain depth is calculated as floor(log2(min(width, height))) - 3, capped at 5 levels, ensuring the smallest mip is at least 8x8 pixels.

SSAO (Screen-Space Ambient Occlusion)

SSAO darkens crevices, corners, and areas where surfaces meet, adding depth and realism to a scene without requiring extra light sources. The implementation samples the depth buffer around each pixel to estimate how much ambient light would be blocked by nearby geometry.

FieldTypeDefaultDescription
ssao_enabledbooltrueEnable SSAO
ssao_radiusf320.5Sample radius in world units (larger = wider darkening)
ssao_intensityf321.0Occlusion strength (higher = darker crevices)

Fog

Distance-based fog blends a configurable fog color into the scene based on pixel depth. Height-based falloff can be layered on top so fog is thicker near the ground and thins out at higher elevations.

FieldTypeDefaultDescription
fog_enabledboolfalseEnable distance fog
fog_color[f32; 3][0.7, 0.75, 0.82]Fog color (linear RGB)
fog_densityf320.02Exponential density factor
fog_startf325.0Distance where fog begins
fog_endf32100.0Distance where fog reaches full opacity
fog_height_enabledboolfalseEnable height-based falloff
fog_height_fallofff320.1How quickly fog thins with altitude
fog_height_originf320.0World Y where fog is thickest

Volumetric Lighting (God Rays)

Volumetric lighting simulates light scattering through participating media (dust, fog, haze), producing visible shafts of light (god rays). The effect ray-marches from each pixel toward the camera, sampling the shadow map at each step to determine whether that point in space is lit or in shadow.

How it works

  1. For each screen pixel, reconstruct its world position from the depth buffer
  2. March volumetric_samples steps along the view ray from the pixel back toward the camera
  3. At each step, project the position into shadow-map space and sample the cascaded shadow map
  4. Accumulate light contribution where the sample is not in shadow, applying exponential decay
  5. The resulting volumetric texture is additively blended into the scene during the composite pass

Because volumetric lighting depends on the shadow map, it requires at least one directional light with shadows enabled. The effect is disabled when shadows are off.

Per-light configuration

Each directional light can control its volumetric contribution independently via its light component:

[entities.sun.light]
type = "directional"
direction = [0.4, 0.6, 0.05]
color = [1.0, 0.92, 0.75]
intensity = 6.0
volumetric_intensity = 4.0               # god ray brightness (0 = no rays)
volumetric_color = [1.0, 0.88, 0.6]      # tint for the light shafts
Light fieldTypeDefaultDescription
volumetric_intensityf320.0Per-light god ray strength (0 = disabled for this light)
volumetric_color[f32; 3]light colorTint color for the shafts from this light

Global scene settings

The [post_process] block controls the overall volumetric pass:

FieldTypeDefaultDescription
volumetric_enabledboolfalseEnable volumetric lighting
volumetric_samplesu3232Ray-march steps per pixel (higher = smoother, more expensive)
volumetric_densityf321.0Scattering density multiplier
volumetric_max_distancef32100.0Maximum ray-march distance from camera
volumetric_decayf320.98Exponential decay per step (closer to 1.0 = shafts extend further)

Example: dungeon window

[post_process]
volumetric_enabled = true
volumetric_samples = 64
volumetric_density = 30.0
volumetric_max_distance = 15.0
volumetric_decay = 0.998
exposure = 2.5

[entities.sun.light]
type = "directional"
direction = [0.4, 0.4, 0.05]
color = [1.0, 0.92, 0.75]
intensity = 6.0
volumetric_intensity = 4.0
volumetric_color = [1.0, 0.88, 0.6]

High volumetric_density with a short volumetric_max_distance and decay close to 1.0 produces thick, concentrated shafts — good for dusty interiors. For outdoor haze, use lower density and longer distance.

Dither

Subtle dithering reduces color banding in gradients (skies, fog falloff, smooth surfaces). A blue-noise pattern is applied during the composite pass.

FieldTypeDefaultDescription
dither_enabledboolfalseEnable dithering
dither_intensityf320.03Dither strength (subtle values like 0.02–0.05 work best)

Scene Configuration

Add a [post_process] block to your scene TOML to configure per-scene settings:

[post_process]
bloom_enabled = true
bloom_intensity = 0.04
bloom_threshold = 1.0
ssao_enabled = true
ssao_radius = 0.5
ssao_intensity = 1.0
fog_enabled = true
fog_density = 0.02
fog_color = [0.7, 0.75, 0.82]
volumetric_enabled = false
vignette_enabled = true
vignette_intensity = 0.3
exposure = 1.0

All fields are optional — omitted values use their defaults.

CLI Flags

Override post-processing settings from the command line:

# Disable all post-processing
flint render scene.toml --no-postprocess

# Adjust bloom
flint render scene.toml --bloom-intensity 0.08 --bloom-threshold 0.8

# Adjust exposure
flint render scene.toml --exposure 1.5

# SSAO
flint render scene.toml --ssao-radius 0.5 --ssao-intensity 1.0

# Fog
flint render scene.toml --fog-density 0.02 --fog-color 0.7,0.75,0.82 --fog-height-falloff 0.1

# Volumetric lighting
flint render scene.toml --volumetric-density 1.0 --volumetric-samples 32

# Dither
flint render scene.toml --dither-intensity 0.03

# Combine flags
flint play scene.toml --bloom-intensity 0.1 --exposure 1.2 --volumetric-density 5.0
FlagDescription
--no-postprocessDisable the entire post-processing pipeline
--no-shadowsDisable shadow mapping (also disables volumetric)
--bloom-intensity <f32>Override bloom intensity
--bloom-threshold <f32>Override bloom brightness threshold
--exposure <f32>Override exposure multiplier
--ssao-radius <f32>Override SSAO sample radius
--ssao-intensity <f32>Override SSAO strength
--fog-density <f32>Override fog density (0 disables fog)
--fog-color <r,g,b>Override fog color
--fog-height-falloff <f32>Enable height fog with given falloff
--volumetric-density <f32>Override volumetric density (0 disables)
--volumetric-samples <u32>Override volumetric ray-march steps
--dither-intensity <f32>Override dither strength

CLI flags take precedence over scene TOML settings.

Runtime Toggles

During gameplay (flint play / flint edit), toggle post-processing effects with keyboard shortcuts:

KeyAction
F5Toggle bloom on/off
F6Toggle entire post-processing pipeline on/off
F7Toggle SSAO on/off
F8Toggle fog on/off
F9Toggle dither on/off
F10Toggle volumetric lighting on/off

When post-processing is toggled off at runtime, the PBR shader’s built-in ACES tonemapping and gamma correction are automatically restored as a fallback path. This means the scene always looks correct regardless of the pipeline state.

Shader Integration

When the post-processing pipeline is active, the engine sets enable_tonemapping = 0 in the PBR uniform buffer, forcing shaders to output raw linear HDR values. The composite pass then applies:

  1. SSAO — darkens ambient-occluded areas using the AO texture
  2. Volumetric — additively blends god ray light shafts
  3. Exposure — multiplies all color values by the exposure setting
  4. ACES tonemapping — maps HDR values to displayable range using the ACES filmic curve
  5. Fog — blends fog color based on depth and optional height falloff
  6. Gamma correction — converts linear light to sRGB
  7. Dither — applies subtle noise to reduce banding
  8. Vignette — darkens screen edges for a cinematic look

When post-processing is disabled (via --no-postprocess or F6), the shader handles tonemapping and gamma internally. This dual-path design ensures backward compatibility with scenes that don’t use post-processing.

Design Decisions

  • Rgba16Float for the HDR buffer provides sufficient precision for bloom extraction without the memory cost of Rgba32Float
  • Progressive downsample/upsample (rather than a single Gaussian blur) produces wide, natural-looking bloom cheaply
  • 1x1 black fallback texture when bloom is disabled avoids conditional bind group creation
  • Resize handlingPostProcessResources are recreated on window resize since the HDR texture and bloom mip chain are resolution-dependent
  • Extended shadow depth — the shadow frustum’s depth range is extended beyond the camera frustum so off-screen casters (ceilings, walls behind the camera) are captured in the shadow map, which is critical for correct volumetric shafts in enclosed spaces

Further Reading