Skip to content

Post-Processing (Volume)

Awaken's render settings are an add/remove stack of optional effects - modelled on Unity's post-process Volume - where an effect that isn't in the stack simply isn't applied, so nothing leaks in behind your back.

Absent means off

In Unity's URP Volume you add a Color Adjustments or Bloom override; anything you don't add stays at the engine default. Awaken takes the same approach (RenderEffects in packages/render/src/render.ts): each effect is an optional field, and an absent effect is not applied at all - it packs neutral values, never a hidden default.

This exists for a concrete reason. Awaken used to have built-in fog and bloom always on, which meant an un-graded Unity import arrived over-exposed and fogged. With the stack, an imported scene that never set fog gets no fog effect. A blank scene starts with just Sky and Ambient present - a clean, un-graded look.

Effects are added and removed from the Render Settings panel via a + Add effect menu; each shows as a card you can tune or remove.

The effects

EffectKeyApplied inWhat it does
Skyskymesh/sky passZenith / horizon / ground gradient; also the fog target colour. Absent → neutral built-in sky
Ambientambientmesh passFlat indirect fill (intensity + colour). Absent → lit by direct light only
Fogfogmesh passDistance fog toward the sky colour (linear start→end, or exp density). Absent → no fog
Color AdjustmentscolorGradecompositePost-exposure (EV), contrast, saturation, hue shift, colour filter
Lift / Gamma / GainliftGammaGaincompositeShadows / mids / highlights wheels (offset, inverse-gamma, gain)
Shadows / Midtones / HighlightsshadowMidHighcompositeTint by luminance region with smoothstep limits
Bloombloombright-pass + compositeBright-pass at threshold, intensity add, scatter spread
VignettevignettecompositeDarken toward the edges (colour, intensity, smoothness)
SSAOssaoSSAO chainScreen-space ambient occlusion (intensity, radius)

Freshly-added effects use neutral defaults (they do nothing visible until you tune them), so adding a card never jolts the image.

A separate base setting, tonemap ("linear" or "aces"), is not an add/remove card - it is always present as a mode. See PBR & Lighting.

Sky, Ambient, and Fog are lit in the mesh pass

Sky, Ambient, and Fog aren't post effects - they feed the lighting directly. Sky colours travel on the camera uniform and drive both the background sky pass and the per-fragment fog blend; Ambient and Fog density travel on the Lights uniform. That is why they take effect during opaque shading rather than in the composite.

How the grade is packed and applied

The five grade-affecting effects (Color Adjustments, Lift/Gamma/Gain, Shadows/Midtones/Highlights, Bloom intensity, Vignette) are packed by packGrade() (packages/render/src/grade.ts) into a single 44-float (GRADE_FLOATS) uniform - 11 vec4s. Absent effects pack neutral values, so an empty stack yields a pure pass-through (neutralGrade()).

That uniform is consumed once, in the composite pass (POST_WGSL), which runs after the scene is drawn and bloom is computed. The order inside the composite mirrors URP:

  1. Add bloom (scene + bloom · intensity)
  2. Post-exposure (2^EV) × colour filter
  3. Contrast about mid-grey
  4. Hue shift (rotation about the achromatic axis)
  5. Saturation (toward luma)
  6. Lift / gamma / gain
  7. Shadows / midtones / highlights by luma region
  8. Vignette toward its colour
  9. Clamp and write the swapchain

So the entire colour grade is a single fullscreen pass reading one 44-float uniform - cheap and fully deterministic.

The SSAO chain

SSAO is opt-in (add the Ambient Occlusion (SSAO) effect). When present, before the main pass the renderer runs a three-stage chain (all in packages/render/src/shader.ts):

  • A prepass writes each opaque fragment's world position and true geometric normal.
  • The AO pass (a fullscreen fragment pass, not a compute kernel) takes 16 hemisphere samples per pixel around the surface normal, comparing projected sample depths against the scene to estimate occlusion within radius metres, with a depth bias to avoid self-occlusion acne.
  • A blur denoises the randomized result.
  • The mesh shader then multiplies ambient only by visibility^intensity (Godot-style). Because visibility is in [0, 1], the exponent only shapes contrast - it can deepen contact shadows but can never drive ambient to pure black. intensity acts like Godot's ssao_power; radius is scene-scale dependent (default 0.6 m).

SSAO is gated by a camera flag (skyZenith.w), so when the effect is absent the mesh shader doesn't sample the AO texture at all - it is a true no-op, not a full-white multiply. It never affects direct/sun lighting (which keeps its own shadows).

Imported render settings

The Godot and Unreal loaders produce a flat render bag; effectsFromFlat() converts it into the stack, adding only the effects the source actually set. A project with fog off (density 0) gets no Fog effect rather than a default one - the whole point of "absent means off." See Import.

📸 Screenshot - save as img/render-postfx-stack.png

The Render Settings panel with several effect cards added (Sky, Ambient, Bloom, Color Adjustments, Vignette) and the + Add effect menu open.

📸 Screenshot - save as img/render-postfx-bloom-ssao.png

A before/after pair of the same scene: left with no effects, right with Bloom and SSAO added, showing the highlight glow and the contact-shadow darkening.

See also

Awaken — browser-native WebGPU game engine.