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.
Common PostFX modding goals:
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 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:
256x1 image.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.
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:
PostFXSSAOGeneralEnabledPostFXSSAOGeneralQualityPostFXScreenSpaceShadowsEnabledPostFXDOFGeneralEnabledPostFXMotionBlurEnabledPostFXMotionBlurStrengthPostFXMotionBlurPlayerVehicleGraphicEVCompensationCustom 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:
ShaderData object that points to HLSL shader files.GFXStateBlockData object that defines depth, blending, and sampler state.PostEffect object that defines shader, textures, target, timing, and priority.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.
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.
scenetree objects or disabled graphics features in Lua, and do not assume optional targets exist on every quality preset.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:
#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.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.
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.
PostEffect has six texture inputs and twelve textureCube inputs.ShaderData must reference registered shader files and a supported pixVersion.renderPriority is sorted descending for effects with the same timing.skip = true skips the effect and its children without disabling siblings.allowReflectPass = true.$backBuffer is common for final composite passes, but multi-pass effects should usually write to $outTex or named intermediate targets first.PostEffect mods should use graphics shader passes.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:
tex2D call has a cost. Group your samples and avoid redundant lookups in loops.if/else inside pixel shaders can lead to “branch divergence,” slowing down the GPU. Use step(), lerp(), or clamp() instead.targetScale to render it at a lower resolution and upscale later.half precision for colors and effects where 32-bit floating point accuracy isn’t required.for loops in the pixel shader unless absolutely necessary (e.g., high-quality Gaussian blurs). Consider approximating effects with fewer samples.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:
#prepass or #ssaoMask contains the expected data without modifying shaders.return float4(normalWS * 0.5 + 0.5, 1); to see normals).#prepass[Depth] is non-linear (device depth). Always use decodeGBuffer or shared helpers to get eye-linear depth for distance calculations.targetScale) but sampling it with UVs meant for full resolution without accounting for the offset/scale.#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..a can lead to missing visual details.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:
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:
Was this article helpful?