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, screen-space shadows, screen space reflections, anti-aliasing, and upscaling.
Since version 0.40, PostFX passes can run as compute shaders instead of fullscreen pixel shader draws. Nearly all shipped effects have moved to compute, and custom effects can use the same path on every renderer, including Direct3D 11.
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 an HLSL compute shader. |
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. |
SMAA_T2X_PostEffect |
SMAA T2x temporal antialiasing and history resolve. The render view supplies its two-phase camera jitter. |
PostFxSSSRObject |
Screen space reflections (trace, denoise, and history). Only present on Direct3D 12 and Vulkan. |
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:
PostFXSSAOGeneralQuality (Off, Normal, High)PostFXSpecularOcclusionEnabledPostFXScreenSpaceShadowsEnabledPostFXSSRQuality (Off, Low, Medium, High) and PostFXSSREnabledPostFXDOFGeneralEnabledPostFXMotionBlurEnabledPostFXMotionBlurStrengthPostFXMotionBlurPlayerVehicleGraphicAntialiasType and GraphicUpscalingQualityGraphicEVCompensationAvailability keys such as PostFXSSRAvailable and PostFXScreenSpaceShadowsAvailable report whether the effect exists on the current renderer and lighting quality. Check them before exposing options in a mod UI.
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:
ShaderData object that points to an HLSL compute shader through DXComputShaderName.GFXStateBlockData object that defines the sampler states.PostEffect object that defines shader, textures, target, timing, and priority.The PostEffect fields are the same whether the effect uses a compute or a pixel shader. The engine checks which shader stages the ShaderData provides: if it has a compute shader, the effect is dispatched; otherwise it is drawn as a fullscreen quad.
Minimal Lua structure, modelled on lua/ge/client/postFx/fxaa.lua:
local pfxDefaultStateBlock = scenetree.findObject("PFX_DefaultStateBlock")
local shader = scenetree.findObject("MyPostFxShader")
if not shader then
shader = createObject("ShaderData")
shader.DXComputShaderName = "shaders/common/postFx/myEffect.cs.hlsl"
shader:setField("samplerNames", 0, "$colorTex")
shader.pixVersion = 5.0
shader:registerObject("MyPostFxShader")
end
local state = scenetree.findObject("MyPostFxStateBlock")
if not state then
state = createObject("GFXStateBlockData")
state:inheritParentFields(pfxDefaultStateBlock)
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
DXComputShaderName, without the e. It is used on every renderer, not only Direct3D.The matching shader is split into a header with the uniforms and samplers, and a .cs.hlsl entry point:
// shaders/common/postFx/myEffect.h.hlsl
#ifndef MY_EFFECT_H_HLSL
#define MY_EFFECT_H_HLSL
#include "shaders/common/postFx/postFx.h.hlsl"
uniform_sampler2D(colorTex, 0);
cbuffer perDraw : register(b0)
{
uniform float strength;
POSTFX_UNIFORMS;
};
#include "shaders/common/postFx/postFx.hlsl"
#endif
// shaders/common/postFx/myEffect.cs.hlsl
#define PFX_COMPUTE 1
#include "myEffect.h.hlsl"
#include "shaders/common/postFx/postFxCompute.h.hlsl"
PFX_NUMTHREADS
void main(uint3 dispatchThreadID : SV_DispatchThreadID)
{
uint2 storePos;
if (!postFxComputeThread(dispatchThreadID, storePos))
{
return;
}
PFXVertToPix IN = processPostFxCompute(dispatchThreadID);
float4 color = tex2D(colorTex, IN.uv0);
float luma = dot(color.rgb, float3(0.299, 0.587, 0.114));
pfxOut[storePos] = lerp(color, float4(luma.xxx, color.a), strength);
}
What the pieces do:
| Element | Purpose |
|---|---|
#define PFX_COMPUTE 1 |
Must come before the first include. It switches POSTFX_UNIFORMS to the compute layout and enables the compute helpers. |
postFxCompute.h.hlsl |
Declares the output texture pfxOut (RWTexture2D<float4> at u0), PFX_NUMTHREADS, and the helper functions. Include it after the effect’s own header so the uniforms are already declared. |
PFX_NUMTHREADS |
Fixed [numthreads(8, 8, 1)]. The engine dispatches enough 8x8 groups to cover the viewport. |
postFxComputeThread(id, storePos) |
Returns the target pixel for this thread and false for threads that overshoot the viewport. Always return early for those. |
postFxComputeUv(id) |
Viewport relative UV, 0 to 1, top left origin. |
processPostFxCompute(id) |
Rebuilds the PFXVertToPix struct (hpos, uv0 to uv3, wsEyeRay) that pixel shader effects received, so an existing effect body can be ported without changes. |
pfxOut[storePos] = ... |
Writes the result. Compute effects have no blend unit and no discard; see the rules below. |
Rules that differ from pixel shader effects:
discard. If a pixel should stay unchanged, sample the input and write it back (see fog.cs.hlsl, which composites over dst instead of blending).target is $backBuffer, the engine writes to the free half of the scene color ping-pong and the input $backBuffer stays readable. For $outTex or a named target, the output must not also appear in texture.SrcAlpha / InvSrcAlpha state block now reads the destination and lerps manually. Blend fields on the state block are ignored.tex2Dlod or explicit gradients for mip-mapped inputs. Compute shaders cannot compute automatic derivatives.zDefined, zEnable, and blend state block fields are irrelevant. Only the sampler states are used.Existing compute effects to learn from: fog.lua / fog.cs.hlsl (manual blend, depth reconstruction), fxaa.lua / fxaa/fxaa.cs.hlsl (minimal single pass), smaa.lua (multi-pass chain with $outTex and named targets), dof.lua (many passes registered through a small registerComputeShader helper), and glow.lua.
Compute Shader Example: CMAA2 walks through a complete multi-pass mod with named targets, a temporal history, velocity and depth inputs, runtime pass switching, and a shader body shared between compute and pixel shader variants.
The older fullscreen quad path still works. Set DXVertexShaderFile and DXPixelShaderFile instead of DXComputShaderName, keep zDefined = true, zEnable = false, zWriteEnable = false on the state block, and use processPostFxVert / SV_Target in HLSL. Use it only when porting old content; new effects should use compute so they match the shipped effects and can write intermediate results directly.
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. |
skipDraw |
Skips only this effect’s own pass. Its children still run with the incoming texture, and its target name stays registered. Use it to toggle optional passes in a chain. |
Custom effects should use existing game effects as reference examples. Good starting points are lua/ge/client/postFx/fxaa.lua, lua/ge/client/postFx/fog.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 until screen space reflections reuse it, and Depth contains device depth. See Reading the prepass
. |
#glowbuffer |
RT0 |
Glow render bin output used by the glow PostFX chain. |
#VelocityBuffer |
RT0, RT1 only with an FSR reactive mask |
RT0 is screen-space motion, used by motion blur, SMAA T2x, upscalers, and screen space reflections. RT1 is an R8 vehicle silhouette (0 static, 1 vehicle) and is only allocated for FSR 3, FSR 4, and FSR 3 (custom). |
#ReactiveMask |
RT0 |
Disocclusion mask built from #VelocityBuffer[RT1] during UpscaleDispatch for FSR 3, FSR 4, and FSR 3 (custom). It marks the band a vehicle just uncovered, not the vehicle itself. DLSS and XeSS do not create it. |
#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. R is ambient occlusion. With specular occlusion enabled the target is RGBA16F: G holds the bent normal accessibility and BA the octahedral bent normal. May be a white fallback when disabled. |
#gtaoOutput |
RT0 |
The same GTAO texture as #ssaoMask, registered for debug tools while GTAO is running. |
#lightinfo |
RT0, RT1 on Advanced Lighting 1.5 |
Deferred light accumulation. RT0 is accumulated diffuse light, RT1 is accumulated specular. The lighting passes consume it in the same frame; do not sample it from a later PostFX. |
#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. |
#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 |
|---|---|---|
#dofAutoFocus, #dofAutoFocusHistory |
DOF auto focus | 1x1 focus probe and its history. Only exist while DOF is enabled. |
#dofPrefilter, #dofPrefilterTemporal, #dofHistory |
DOF prefilter | Half resolution prefiltered color, its temporal resolve, and the history copy. |
#dofPrefilterCoarse, #dofPrefilterCoarse2 |
DOF prefilter | Further downsampled copies of the prefilter. |
#dofCocTile, #dofCocTileDilated |
DOF | Circle of confusion tiles used by the bokeh pass. |
#dofBokeh |
DOF bokeh | Bokeh image composited over the scene. |
#edge |
EdgeAAPostEffect |
Edge mask for edge AA/debug. |
#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, until screen space reflections reuse the target. |
#prepass[RT1] |
A |
Retroreflectivity coefficient for opaque materials. Translucent prepass draws store coverage here instead. |
#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.
When screen space reflections are enabled, the SssrSurfaces bin runs immediately after lighting and rewrites #prepass[RT1] in place. From then until the next frame’s prepass, that slot is packed reflection receiver data, not specular color or retroreflectivity. #prepass[RT0] and #prepass[Depth] are not rewritten. With reflections disabled, RT1 keeps the prepass contents for the whole frame. A PostFX that needs specular or retroreflectivity has to run before SssrSurfaces.
In HLSL, include the shared PostFX and G-buffer helpers. The header declares the samplers and uniforms, the .cs.hlsl file does the work:
// myPrepassDebug.h.hlsl
#include "shaders/common/postFx/postFx.h.hlsl"
#include "shaders/common/gbuffer.hlsl"
uniform_sampler2D(prepassTex, 0);
uniform_sampler2D(prepassDepthTex, 1);
cbuffer perDraw : register(b0)
{
POSTFX_UNIFORMS;
};
#include "shaders/common/postFx/postFx.hlsl"
// myPrepassDebug.cs.hlsl
#define PFX_COMPUTE 1
#include "myPrepassDebug.h.hlsl"
#include "shaders/common/postFx/postFxCompute.h.hlsl"
PFX_NUMTHREADS
void main(uint3 dispatchThreadID : SV_DispatchThreadID)
{
uint2 storePos;
if (!postFxComputeThread(dispatchThreadID, storePos))
{
return;
}
PFXVertToPix IN = processPostFxCompute(dispatchThreadID);
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;
pfxOut[storePos] = 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 until the SssrSurfaces bin reuses it.#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. |
Upscaling and jitter constants (see Jitter and jittered UVs ):
| Constant | Meaning |
|---|---|
sceneJitterUV |
Current sub-pixel camera jitter in normalized viewport units. Non-zero only while a temporal upscaler or SMAA T2x is active. Depth, normals, and velocity always carry this jitter. |
sceneColorJitterUV |
Same as sceneJitterUV until the upscaler has resolved the scene color, then 0. Use it when combining scene color with a buffer that was rendered with jitter. |
projectionOffset |
Camera projection offset in clip space, including the jitter while it is active. |
Compute-only constants (declared by POSTFX_UNIFORMS when PFX_COMPUTE is defined):
| Constant | Meaning |
|---|---|
dispatchRect |
xy is the viewport origin in target pixels, zw the viewport size. postFxComputeThread uses it to map threads to pixels. |
eyeRayTopLeft, eyeRayRight, eyeRayDown |
World-space eye ray corners. processPostFxCompute interpolates them into wsEyeRay, replacing what the vertex shader used to output. |
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, local exposure, and depth of field. |
PFXAfterBin |
Run after a named render bin. Requires renderBin. Used by 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. The effect owns its target, so the contents persist between frames; as a child in a chain it still runs in chain order, which makes it the usual way to store a history texture. |
Common render bins used by shipped effects:
| Render bin | Examples |
|---|---|
SssrTrace |
Screen space reflection trace and denoise. Runs after lighting and before the color pass. |
ObjTranslucentBin |
Fog, underwater fog, caustics, edge detection. |
GlowBin |
Glow, turbulence. |
UpscaleDispatch |
Temporal upscaler dispatch (DLSS, FSR, XeSS). DOF runs PFXBeforeBin here so it is applied at render resolution before upscaling. This bin is a no-op for FXAA, SMAA, and SMAA T2x. |
AfterPostFX |
Gamma/color correction, HDR/local exposure, and the SMAA T2x resolve. Runs at display resolution after a temporal upscale and at render resolution for deferred modes. |
OverlayRender |
Motion blur runs before this bin so billboards and icons stay sharp; masked screen blur runs after it. |
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. skipDraw = true skips only the effect’s own pass.PFXTexGenOnDemand effect, which owns its target.allowReflectPass = true.$backBuffer is common for final composite passes, but multi-pass effects should usually write to $outTex or named intermediate targets first.pfxOut at u0 and must not sample their own output. They always run with 8x8 thread groups.When an upscaler or SMAA T2x is active, the scene is rendered at a reduced or jittered resolution and converted to display resolution in the UpscaleDispatch bin. This changes what a custom effect sees:
UpscaleDispatch (SssrTrace, ObjTranslucentBin, GlowBin, and so on) run at render resolution. targetSize is smaller than the window and the camera projection contains a sub-pixel jitter.AfterPostFX or later, and PFXAfterDiffuse effects, run at display resolution on the upscaled image.Temporal Upscaling describes the full chain, the jitter sequence, the reactive mask, and when the history resets.
#VelocityBuffer[RT0] is at render resolution. Temporal effects should reproject with matPrevScreenToWorld and expect the jitter offset. While motion blur is enabled, the bin after AfterPostFX repaints vehicle pixels in that slot with the motion-blur velocity. Effects before that bin, including the upscaler and screen space reflections, still see the true velocity.With FXAA or SMAA and a render resolution below 1.0, the image is stretched bilinearly after tone mapping instead. See Anti-aliasing and upscaling
.
Temporal techniques need each frame to sample the scene at a slightly different sub-pixel position so that, over several frames, the history covers the whole pixel. The engine applies this jitter as an offset to the camera projection for the whole frame, not as a texture offset:
| Mode | Jitter pattern |
|---|---|
| FSR, XeSS, FSR 3 (custom) | Halton (2, 3) sequence in the range -0.5 to +0.5 render pixels. The sequence length grows with the upscale ratio. |
| DLSS | The same Halton pattern with an eight-times longer sequence to avoid a screen-fixed dot pattern during long accumulation. |
| SMAA T2x | Two diagonal phases of ±0.25 pixel, alternating every frame. |
| FXAA, SMAA, off | No jitter. |
Every render-resolution bin, including the prepass, velocity buffer, lighting, and all post effects up to UpscaleDispatch, draws with the jittered projection. The upscaler removes the jitter from the color when it resolves the history. The jitter is dropped from the shared camera constants only in the UpscaleOverlay bin, after the last render-resolution effect has run, so editor overlays and the UI are drawn pixel stable.
For a custom effect this means:
#prepass[RT0], #prepass[Depth], and #VelocityBuffer contain the jittered raster, even for effects that run at display resolution after upscaling.UpscaleDispatch the color buffer has the same jitter as depth. After it, the color is stable while depth is not.sceneJitterUV before sampling a jittered buffer. A buffer that was written before the resolve has to be shifted by -sceneColorJitterUV when it is composited over resolved color.// sample the jittered depth buffer at the stable screen center
float2 probeUV = 0.5 + sceneJitterUV;
float ndc = tex2Dlod(prepassDepthTex, float4(viewportCoordToRenderTarget(probeUV, rtParams1), 0, 0)).r;
// composite a bokeh buffer written before the resolve over resolved scene color
float4 bokeh = tex2D(bokehTex, IN.uv0 - sceneColorJitterUV * rtParams0.zw);
rtParams0.zw converts the viewport-relative jitter into the texture’s own UV space, which matters when the input was rendered with targetScale below 1. The shipped depth of field chain (dof/DOF_AutoFocus.cs.hlsl, dof/DOF_Bokeh.cs.hlsl, dof/DOF_Composite.cs.hlsl, dof/DOF_Temporal.cs.hlsl) uses both constants and is the reference for these cases.
From Lua the current jitter can be read for debugging through RenderView.upscaleJitterPixels on the active render view. It is measured in render-resolution pixels.
UpscaleDispatch, or blend them in at display resolution.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 shaders can lead to “branch divergence,” slowing down the GPU. Use step(), lerp(), or clamp() instead.groupshared memory once and reuse it for every tap, which is the main advantage over the old pixel shader path.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], #VelocityBuffer[RT1], or #ReactiveMask are available on every graphics preset and upscaler. RT1 of the prepass only exists on Advanced Lighting 1.5, and the reactive mask only exists for FSR 3, FSR 4, and FSR 3 (custom). Always check if the object exists in Lua before enabling an effect that depends on a specific target.#prepass[RT1] after lighting: With screen space reflections enabled, that slot no longer holds specular color once the SssrSurfaces bin has run. Sample it before that bin, or use #prepass[RT0] and #prepass[Depth], which stay intact..a can lead to missing visual details.pfxOut leaves stale data in that pixel. Only the postFxComputeThread overshoot check may return without writing.texture reads undefined data. Ping-pong through $outTex or a second named target instead.sceneColorJitterUV, produces a one pixel wobble that is only visible with a temporal upscaler or SMAA T2x enabled. Test custom effects with both an upscaler and FXAA. See Jitter and jittered UVs
.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, reflections, and other effects. Screen space reflections and specular occlusion are off in every preset and need High or Ultra Lighting Quality; screen space reflections additionally need Direct3D 12 or Vulkan.
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?