Post Process Effects

PostFX are post-processing effects applied after the scene is rendered. They shape the final image through effects such as ambient occlusion, bloom, color correction, depth of field, motion blur, local exposure, and screen-space shadows.

For modders, PostFX are best used to tune presentation, not to replace lighting. Set up the scene with physically meaningful lights, emissives, materials, exposure, and tone mapping first, then use PostFX for final image treatment.

Screen-space shadows use the depth visible from the current camera to add fine contact shadowing and shadows from distant visible geometry. They complement regular shadow maps, but cannot replace shadows from geometry outside the camera view.

The most stable modding paths are custom color correction ramps and Lua extensions that adjust existing PostFX objects. Creating new fullscreen effects with custom shaders is possible, but it is an advanced workflow and can break when the rendering pipeline changes.

What you can mod

Common PostFX modding goals:

  • Add a custom color filter or grade.
  • Change PostFX values when a level, scenario, camera mode, or tool starts.
  • Temporarily enable or disable existing effects.
  • Create a custom shader-based fullscreen effect.

Recommended approaches:

Goal Recommended method
Color filter or LUT Add a 256x1 PNG under art/postfx/.
Level or scenario-specific tuning Use a Lua extension to adjust existing PostFX objects at runtime.
Temporary camera effect Enable, disable, or set values on existing scenetree PostFX objects.
New visual effect Create a custom PostEffect with ShaderData, GFXStateBlockData, and HLSL shaders.

Color correction ramps

Color correction ramps are the simplest PostFX mod. They are one-dimensional PNG lookup textures used by the color correction pass.

Place custom ramps in:

art/postfx/

For example, a packed mod can contain:

my_postfx_mod.zip
art/
  postfx/
    my_color_grade.png

Ramp requirements:

  • Use PNG format.
  • Use a 256x1 image.
  • Start from the default color ramp when creating a grade.
  • Keep the grade subtle enough that exposure, sky color, headlights, UI readability, and night driving remain usable.

The game discovers color correction ramps by searching art/postfx/*.png. The same path is used by Photo Mode filters, so a ramp placed there can be used as a custom filter.

See Photomode for a practical color-ramp workflow.

Runtime tuning from Lua

Existing PostFX objects can be found through the scene tree and adjusted from Lua. This is useful for scenarios, extensions, tools, and level-specific presentation.

Typical pattern:

local dof = scenetree.findObject("DOFPostEffect")
if dof then
  dof:setFocusParams(0.0, 0.25, 50, 250, -5, 5)
  dof:setAutoFocus(false)
  dof:setFocalDist(120)
  dof:enable()
end

For simple enable/disable behavior:

local ssao = scenetree.findObject("SSAOPostFx")
if ssao then
  ssao:enable()
end

local motionBlur = scenetree.findObject("PostFxMotionBlur")
if motionBlur then
  motionBlur.strength = 0.5
  motionBlur:enable()
end

Important object names:

Object Main use
SSAOPostFx Screen-space ambient occlusion.
ScreenSpaceShadowsPostFx Screen-space shadow mask.
DOFPostEffect Depth of field chain.
PostFxMotionBlur Current motion blur implementation used by options and Photo Mode.
PostEffectBloomObject Bloom threshold and knee.
PostEffectCombinePassObject Final HDR combine, color correction, and tone mapping related values.
PostEffectLocalExposureObject Local exposure and exposure bias.
GammaPostFX Legacy gamma and color ramp effect.
FogPostFx Atmospheric fog post effect.
UnderwaterFogPostFx Underwater fog pass.
ChromaticLensPostFX Chromatic lens effect.
GlowPostFx Glow pass.
FXAA_PostEffect FXAA antialiasing pass.
SMAA_PostEffect SMAA antialiasing chain.

Use the graphics settings API when you want to change the same values exposed by the Options menu. Direct object edits are better for temporary effects, editor tools, and custom camera behavior.

Common graphics setting keys include:

  • PostFXSSAOGeneralEnabled
  • PostFXSSAOGeneralQuality
  • PostFXScreenSpaceShadowsEnabled
  • PostFXDOFGeneralEnabled
  • PostFXMotionBlurEnabled
  • PostFXMotionBlurStrength
  • PostFXMotionBlurPlayerVehicle
  • GraphicEVCompensation

Creating custom shader PostEffects

Custom shader-based effects are built from engine PostEffect objects. This is the path to use when a color ramp or runtime tuning cannot express the effect.

A custom effect usually needs:

  • A ShaderData object that points to HLSL shader files.
  • A GFXStateBlockData object that defines depth, blending, and sampler state.
  • A PostEffect object that defines shader, textures, target, timing, and priority.
  • An optional callback table for shader constants and enable checks.

Minimal Lua structure:

local shader = scenetree.findObject("MyPostFxShader")
if not shader then
  shader = createObject("ShaderData")
  shader.DXVertexShaderFile = "shaders/common/postFx/myEffect.hlsl"
  shader.DXPixelShaderFile = "shaders/common/postFx/myEffect.hlsl"
  shader.pixVersion = 5.0
  shader:registerObject("MyPostFxShader")
end

local state = scenetree.findObject("MyPostFxStateBlock")
if not state then
  state = createObject("GFXStateBlockData")
  state.zDefined = true
  state.zEnable = false
  state.zWriteEnable = false
  state.samplersDefined = true
  state:setField("samplerStates", 0, "SamplerClampLinear")
  state:registerObject("MyPostFxStateBlock")
end

local callbacks = {}
callbacks.onEnabled = function()
  return true
end
callbacks.setShaderConsts = function()
  local fx = scenetree.findObject("MyPostFx")
  if fx then
    fx:setShaderConst("$strength", 1.0)
  end
end
rawset(_G, "MyPostFxCallbacks", callbacks)

local fx = scenetree.findObject("MyPostFx")
if not fx then
  fx = createObject("PostEffect")
  fx:setField("shader", 0, "MyPostFxShader")
  fx:setField("stateBlock", 0, "MyPostFxStateBlock")
  fx:setField("texture", 0, "$backBuffer")
  fx:setField("target", 0, "$backBuffer")
  fx:setField("renderTime", 0, "PFXAfterDiffuse")
  fx:setField("renderPriority", 0, "1")
  fx:registerObject("MyPostFx")
end

Useful PostEffect fields:

Field Description
shader Registered ShaderData object name.
stateBlock Registered GFXStateBlockData object name.
texture Input texture stages, up to six inputs.
target Output target, such as $backBuffer, $outTex, or a named target.
targetScale Relative render target size.
targetSize Absolute render target size.
targetFormat Render target format.
renderTime When the effect runs in the frame.
renderBin Render bin name for bin-timed effects.
renderPriority Order for effects with the same timing. Higher values run earlier.
allowReflectPass Whether the effect also runs during reflection passes.
skip Skips the effect and its children without disabling the parent chain.

Custom effects should use existing game effects as reference examples. Good starting points are lua/ge/client/postFx/fog.lua, lua/ge/client/postFx/chromaticLens.lua, lua/ge/client/postFx/caustics.lua, lua/ge/client/postFx/smaa.lua, and lua/ge/client/postFx/dof.lua.

Render targets and buffers

PostFX shaders read textures through the texture fields on the PostEffect. A texture can be a special token, a named render target, or a file path.

Special tokens:

Identifier Meaning Notes
$backBuffer Current rendered color buffer. Use for fullscreen color effects and final composite passes.
$inTex Chain input from the previous pass. Use inside multi-pass effects.
$outTex Temporary chain output. Use when a pass should feed the next child pass.
$skyCameraVolume Sky camera volume. Falls back to a small black volume if unavailable.

Named render targets use #name, #name[RT0], #name[RT1], or #name[Depth]. Slots RT0 through RT7 and Depth are supported by the engine, but only slots actually filled by the producer are valid.

Common scene buffers:

Target Available slots Purpose
#prepass RT0, RT1 on Advanced Lighting 1.5, Depth Main opaque prepass / G-buffer. RT0 contains normals and roughness, RT1 contains specular and retroreflective material data, and Depth contains device depth.
#glowbuffer RT0 Glow render bin output used by the glow PostFX chain.
#VelocityBuffer RT0 Velocity buffer used by motion blur and temporal effects.
#AnnotationBuffer RT0, Depth Annotation render target used by annotation/sensor tooling. Not a normal gameplay PostFX input.
#waterDepthGradMap RT0 Water depth gradient map used by underwater fog and water effects.
#skyCameraVolume RT0 Volumetric sky/fog data. Prefer $skyCameraVolume so the engine fallback is used when missing.
#cloudTransmittanceCookie RT0 Cloud shadow/transmittance cookie when available.

Lighting and PostFX masks:

Target Available slots Purpose
#ssaoMask RT0 SSAO/GTAO output mask. May be a white fallback when disabled.
#sssMask RT0 Screen-space shadows mask. May be a white fallback when disabled.
#sunShadowMask RT0 Sun shadow mask used by the advanced lighting path. May be a white fallback.
#adaptedLum RT0 Adapted luminance used by HDR combine and exposure.
#bloomFinal RT0 Final bloom texture when bloom is active.
#bucketTex RT0 Luminance histogram bucket texture. Internal HDR/exposure data.
#luminanceHistogram RT0 Luminance histogram texture. Internal HDR/exposure data.
#localExposureBlurredLogLum RT0 Local exposure blurred log luminance texture.
#localExposureBilateralGrid RT0 Local exposure bilateral grid texture.

Effect-local targets created by shipped PostFX:

Target Created by Purpose
#shrunk DOFPostEffect Downsampled DOF source.
#largeBlur DOFBlurX Large blur texture for DOF.
#edge EdgeAAPostEffect Edge mask for edge AA/debug.
#ssao_pow_table SSAOPowTablePostFx On-demand SSAO power table.
#screenBlurMask masked screen blur Mask used by the masked blur effect.
#postEffectDebugOutput post effect debug tool Editor/debug output.

Sensor and tool targets such as #LIDAR_Viz_Texture, #RadarGBuffer, and #UltrasonicGBuffer exist in some builds or tools. Do not depend on them for normal graphics mods.

Named targets are only valid when the system that creates them has run and the current graphics path supports them. Always handle missing scenetree objects or disabled graphics features in Lua, and do not assume optional targets exist on every quality preset.

Reading the prepass

Most depth, normal, and roughness effects use the prepass:

fx:setField("texture", 0, "#prepass[RT0]")
fx:setField("texture", 1, "#prepass[Depth]")

The current Advanced Lighting 1.5 prepass has the following layout:

Slot Channels Contents
#prepass[RT0] R, G Octahedrally encoded world-space normal. Decode it with decodeNormalXY(...).
#prepass[RT0] B Per-pixel roughness in the 0 to 1 range. This is the roughness buffer: sample .z or .b. Lighting clamps it to at least 0.02 when decoding.
#prepass[RT0] A Subsurface intensity for opaque materials. Translucent prepass variants can use it for opacity.
#prepass[RT1] R, G, B Specular color.
#prepass[RT1] A Retroreflectivity coefficient.
#prepass[Depth] R Non-linear device/NDC depth. Convert it with the shared depth helpers and projParams; do not treat it as distance in metres.

RT2 through RT7 are not populated by #prepass. The target syntax supports those slot names generically, but that does not mean every named target provides every slot. On the older Advanced Lighting path, the prepass only provides RT0 and Depth.

In HLSL, include the shared PostFX and G-buffer helpers:

#include "shaders/common/postFx/postFx.h.hlsl"
#include "shaders/common/gbuffer.hlsl"

uniform_sampler2D(prepassTex, 0);
uniform_sampler2D(prepassDepthTex, 1);

struct Appdata
{
    float4 hpos : SV_Position;
    float2 uv0 : TEXCOORD0;
};

float4 main(Appdata IN) : SV_Target
{
    float4 packedPrepass = tex2Dlod(prepassTex, float4(IN.uv0, 0, 0));
    float4 gbuffer = decodeGBuffer(
        TEX_SAMPLER(prepassDepthTex),
        TEX_SAMPLER(prepassTex),
        IN.uv0,
        projParams
    );

    float3 normalWS = gbuffer.xyz;
    float roughness = packedPrepass.z;
    return float4(normalWS * 0.5 + 0.5, roughness);
}

The example displays the decoded normal as RGB and roughness as alpha. Eye-linear depth is available in gbuffer.w for effects that need it. Use the helper functions in shaders/common/gbuffer.hlsl for normals and depth instead of manually decoding them. The prepass layout depends on the lighting path and can change.

Useful prepass facts:

  • Roughness is #prepass[RT0].b, not a separate named target.
  • #prepass[RT1] is present on the Advanced Lighting 1.5 path and carries specular color plus retroreflectivity.
  • #prepass[Depth] is a copied depth texture that can be sampled by PostFX after the prepass.
  • decodeGBuffer(...) returns world-space normal in xyz and eye-linear depth in w for the common PostFX helper overload.
  • projParams, nearFar, and invNearFar are auto-bound when the shader declares them.

Shader constants

PostEffect automatically binds several constants if the shader declares them. Unused constants do not need to be declared.

Target and viewport constants:

Constant Meaning
targetSize Active output target size in pixels.
oneOverTargetSize 1 / targetSize.
texSize0 through texSize5 Size of bound texture stages.
rtParams0 through rtParams5 Viewport-to-render-target transform for texture stages.
targetViewport Active viewport rectangle in normalized target space.
projParams Projection parameters used by depth conversion helpers.

Camera and transform constants:

Constant Meaning
eyePosWorld Diffuse camera position.
vEye View vector.
nearFar Near and far plane.
invNearFar Reciprocal near and far plane.
worldToScreenScale World-to-screen scale.
projectionOffset Camera projection offset.
matWorldToScreen Matrix used by existing PostFX for world/screen conversion.
matScreenToWorld Matrix used by existing PostFX for screen/world conversion.
matPrevScreenToWorld Previous-frame matrix for temporal/reprojection effects.
invCameraMat Inverse camera matrix.

Lighting, fog, water, and time constants:

Constant Meaning
fogData Fog density, offset, and atmosphere height data.
fogColor Current fog color.
fogScatterParams Fog scattering helper parameters.
ambientSH9 Sky ambient spherical harmonics coefficients.
ambientColor Scene ambient color.
sunDirection Direction toward the sun.
sunRadiance Sun radiance after exposure and sky factor.
sunIrradiance Sun irradiance outside atmosphere.
sunColor Sun light color.
screenSunPos Sun position in normalized screen space.
lightDirection Main light direction.
camForward Camera forward vector.
exposure Last frame exposure.
atmosphericFogEnabled Atmospheric fog enable factor.
waterColor Current water fog color.
waterFogData Water fog density, offset, wet depth, and wet darkening.
waterFogPlane Water fog plane.
waterDepthGradMax Water depth gradient maximum.
turbulenceMultiplier Underwater turbulence amount.
accumTime / deltaTime Simulation time and delta.
accumRealTime / deltaRealTime Real time and delta.

Custom constants can be set from Lua callbacks:

callbacks.setShaderConsts = function()
  local fx = scenetree.findObject("MyPostFx")
  if fx then
    fx:setShaderConst("$strength", 1.0)
    fx:setShaderConst("$tint", "1 0.9 0.8 1")
  end
end

Supported script-set constant shapes are float, float2, float3, float4, and float4 arrays. Prefix shader constant names with $ when calling setShaderConst.

Timing and chaining

PostFX timing decides what buffers are already available and what later systems will see.

renderTime Use
PFXBeforeBin Run before a named render bin. Requires renderBin. Used by fog, caustics, gamma/HDR-related passes, and local exposure.
PFXAfterBin Run after a named render bin. Requires renderBin. Used by DOF, glow, turbulence, and masked blur.
PFXAfterDiffuse Run after the diffuse scene pass. Good for simple fullscreen effects and antialiasing-style passes.
PFXEndOfFrame Run at the end of the frame. Use only when you specifically need final-frame timing.
PFXTexGenOnDemand Do not schedule every frame. Generate a named texture only when requested.

Common render bins used by shipped effects:

Render bin Examples
ObjTranslucentBin Fog, underwater fog, caustics, edge detection.
GlowBin DOF, glow, turbulence.
AfterPostFX Gamma/color correction and HDR/local exposure related passes.
OverlayRender Masked screen blur.

For a multi-pass effect, make a parent PostEffect and add child effects. Write intermediate passes to $outTex or a named #target, then read $inTex in the next pass. Use a named target when another effect or debug view needs to sample the result by name.

Custom shader constraints

  • A generic PostEffect has six texture inputs and twelve textureCube inputs.
  • ShaderData must reference registered shader files and a supported pixVersion.
  • Missing named targets make the effect invalid until the target exists.
  • renderPriority is sorted descending for effects with the same timing.
  • skip = true skips the effect and its children without disabling siblings.
  • Reflection passes skip most PostFX unless allowReflectPass = true.
  • Writing directly to $backBuffer is common for final composite passes, but multi-pass effects should usually write to $outTex or named intermediate targets first.
  • Compute-shader PostFX are possible through engine-side specialized classes, but generic Lua PostEffect mods should use graphics shader passes.
  • Avoid depending on internal HDR/exposure targets unless the mod is tied to the current renderer version.

Performance Considerations

PostFX effects run every frame for every pixel on the screen. Even a seemingly simple shader can significantly impact performance, especially at high resolutions (4K) or on lower-end GPUs.

Optimization tips:

  • Minimize texture fetches: Every tex2D call has a cost. Group your samples and avoid redundant lookups in loops.
  • Avoid complex branching: Heavy use of if/else inside pixel shaders can lead to “branch divergence,” slowing down the GPU. Use step(), lerp(), or clamp() instead.
  • Watch target scale: If an effect does not require full resolution (e.g., a blur pass), use targetScale to render it at a lower resolution and upscale later.
  • Precision: Use half precision for colors and effects where 32-bit floating point accuracy isn’t required.
  • Avoid heavy loops: Avoid long for loops in the pixel shader unless absolutely necessary (e.g., high-quality Gaussian blurs). Consider approximating effects with fewer samples.

Debugging your Effects

Debugging PostFX is challenging because you cannot step through GPU code. The primary method is “visual debugging”—outputting raw data as colors to the screen.

Techniques:

  • NamedTextureTarget Viewer: Use this editor tool to browse and preview all active render targets (NamedTexTargets) in real-time. It supports pan/zoom and channel filtering, making it the fastest way to verify if a buffer like #prepass or #ssaoMask contains the expected data without modifying shaders.
  • Visualizing buffers: Instead of calculating a complex effect, return the value of a specific buffer (e.g., return float4(normalWS * 0.5 + 0.5, 1); to see normals).
  • Color coding values: Map a scalar value (like depth or roughness) to a color ramp to identify precision issues or clamping.
  • Binary search for errors: If a shader fails to compile or causes a crash, comment out sections of the code until you find the problematic line.

Common Pitfalls

  • Linear vs Non-linear Depth: Forgetting that #prepass[Depth] is non-linear (device depth). Always use decodeGBuffer or shared helpers to get eye-linear depth for distance calculations.
  • Resolution Mismatches: Using a downsampled target (via targetScale) but sampling it with UVs meant for full resolution without accounting for the offset/scale.
  • Assuming Target Existence: Assuming #prepass[RT1] or other buffers are available on all graphics presets. Always check if the object exists in Lua before enabling an effect that depends on a specific target.
  • Ignoring Alpha Channels: Some targets store critical data (like subsurface intensity or retroreflectivity) in the alpha channel; forgetting to sample .a can lead to missing visual details.

Loading and persistence

At startup, the game initializes the Lua PostFX modules and creates the standard PostFX objects. Photo Mode keeps a temporary backup of values it changes so they can be restored when leaving Photo Mode.

This has a few practical consequences for mods:

  • Prefer temporary runtime changes for scenarios, camera tools, and gameplay effects.
  • Restore any temporary changes when your extension unloads or the effect is no longer needed.

Quality settings

PostFX availability depends on graphics settings. Lower presets can disable or reduce SSAO, screen-space shadows, bloom, depth of field, motion blur, reflections, and other effects.

Do not build essential gameplay visibility around a subtle PostFX effect. A level should remain readable when lower graphics presets reduce or disable PostFX.

Recommended checks:

  1. Author and tune on High or Ultra.
  2. Check Normal for typical gameplay.
  3. Check Low and Lowest for readability.
  4. Test day, dusk, night, tunnels, forests, interiors, and weather transitions.
  5. Verify SDR and HDR output if the content depends on bright highlights.

Best practices

  • Use PostFX after lighting, exposure, and tone mapping are already in a good state.
  • Keep color grading subtle enough for gameplay.
  • Avoid using bloom as the only way to communicate that a light is bright.
  • Avoid heavy depth of field or motion blur during normal driving.
  • Keep SSAO and screen-space shadows moderate so they ground objects without dirtying the image.
  • Prefer color ramps for shareable looks.
  • Prefer Lua runtime tuning for temporary camera, scenario, or level behavior.
  • Treat custom shader effects as advanced and version-sensitive.

Related pages

Last modified: July 23, 2026

Any further questions?

Join our discord
Our documentation is currently incomplete and undergoing active development. If you have any questions or feedback, please visit this forum thread.