Skip to content

Post-Processing

The PostProcess API applies full-screen shader effects after each camera renders. Effects are managed through PostProcessStack objects that can be bound to individual cameras, allowing different cameras to have different effects.

Quick Start

import { PostProcess } from 'esengine';
// Create an effect stack
const fx = PostProcess.createStack();
fx.addPass('vignette', PostProcess.createVignette());
fx.setUniform('vignette', 'u_intensity', 0.6);
fx.setUniform('vignette', 'u_softness', 0.5);
// Bind to a camera entity
PostProcess.bind(cameraEntity, fx);

PostProcessStack

A PostProcessStack is a reusable effect chain. Create one, add passes, and bind it to one or more cameras:

const fx = PostProcess.createStack();
// Add effects (execute in order)
fx.addPass('blur', PostProcess.createBlur());
fx.addPass('vignette', PostProcess.createVignette());
// Configure uniforms
fx.setUniform('blur', 'u_intensity', 3.0);
fx.setUniform('vignette', 'u_intensity', 0.8);
fx.setUniform('vignette', 'u_softness', 0.3);
// Bind to camera — effects apply immediately
PostProcess.bind(cameraEntity, fx);

Stack Methods

MethodDescription
addPass(name, shader)Append a named pass
removePass(name)Remove a pass by name
clearPasses()Remove all passes
setEnabled(name, enabled)Enable or disable a pass
setUniform(pass, name, value)Set a float uniform
setUniformVec4(pass, name, value)Set a vec4 uniform
setAllPassesEnabled(enabled)Enable or disable all passes at once
passCountTotal number of passes
enabledPassCountNumber of enabled passes
destroy()Destroy the stack (auto-unbinds all cameras)

Camera Binding

// Bind a stack to a camera
PostProcess.bind(cameraEntity, fx);
// Query the current stack
const stack = PostProcess.getStack(cameraEntity); // null if none
// Unbind (camera renders without effects)
PostProcess.unbind(cameraEntity);

Built-in Effects

MethodEffectUniforms
createBloomExtract()Bloom: extract bright pixelsu_threshold (0–1) — brightness cutoff
createBloomKawase(iteration)Bloom: Kawase blur passu_radius — blur spread, u_resolution (auto)
createBloomComposite()Bloom: recombine with sceneu_intensity — glow strength, u_sceneTexture (auto)
createBlur()Gaussian bluru_intensity — blur spread
createVignette()Darkened edgesu_intensity (0–1) — effect strength, u_softness (0–1) — falloff
createGrayscale()Desaturationu_intensity (0–1) — blend ratio
createChromaticAberration()RGB channel offsetu_intensity — offset amount

Bloom

Bloom uses a multi-pass pipeline: extract bright pixels, apply several Kawase blur iterations, then composite the result back onto the scene. This produces higher-quality results than a single-pass approach:

const fx = PostProcess.createStack();
// 1. Extract bright pixels
fx.addPass('bloom_extract', PostProcess.createBloomExtract());
fx.setUniform('bloom_extract', 'u_threshold', 0.4);
// 2. Kawase blur (multiple iterations for wider, smoother blur)
for (let i = 0; i < 5; i++) {
fx.addPass(`bloom_kawase_${i}`, PostProcess.createBloomKawase(i));
fx.setUniform(`bloom_kawase_${i}`, 'u_radius', 1.0);
}
// 3. Composite bloom back onto the original scene
fx.addPass('bloom_composite', PostProcess.createBloomComposite());
fx.setUniform('bloom_composite', 'u_intensity', 1.5);
PostProcess.bind(cameraEntity, fx);

Blur

9-tap Gaussian blur. Higher u_intensity increases the blur spread:

const fx = PostProcess.createStack();
fx.addPass('blur', PostProcess.createBlur());
fx.setUniform('blur', 'u_intensity', 3.0);
PostProcess.bind(cameraEntity, fx);

Vignette

Darkens the screen edges:

const fx = PostProcess.createStack();
fx.addPass('vignette', PostProcess.createVignette());
fx.setUniform('vignette', 'u_intensity', 0.6);
fx.setUniform('vignette', 'u_softness', 0.5);
PostProcess.bind(cameraEntity, fx);

Grayscale

Blends between the original color and grayscale:

const fx = PostProcess.createStack();
fx.addPass('grayscale', PostProcess.createGrayscale());
fx.setUniform('grayscale', 'u_intensity', 1.0); // fully gray
PostProcess.bind(cameraEntity, fx);

Chromatic Aberration

Offsets the R and B channels for a lens distortion look:

const fx = PostProcess.createStack();
fx.addPass('chromatic', PostProcess.createChromaticAberration());
fx.setUniform('chromatic', 'u_intensity', 2.0);
PostProcess.bind(cameraEntity, fx);

Screen Post-Processing

Screen-level effects apply after all cameras have rendered, affecting the final composited image. This is useful for global effects like fade-to-black, full-screen vignette, or cinematic letterboxing.

import { PostProcess } from 'esengine';
const screenFx = PostProcess.createStack();
screenFx.addPass('vignette', PostProcess.createVignette());
screenFx.setUniform('vignette', 'u_intensity', 0.8);
// Apply to the final composited image (after all cameras)
PostProcess.setScreenStack(screenFx);
// Remove screen effects
PostProcess.setScreenStack(null);

Post-Process Volumes

Volumes allow spatial post-processing — effects that activate when the camera enters a region. Multiple volumes blend by priority, similar to Unity’s Post Process Volumes.

Volume Properties

PropertyTypeDefaultDescription
isGlobalbooleantrueApplies everywhere regardless of shape
shape'box' | 'sphere''box'Trigger region shape
size{ x, y }{ x: 5, y: 5 }Half-extents (box) or radius (sphere)
prioritynumber0Higher priority overrides lower
weightnumber1Blend weight (0–1)
blendDistancenumber0Smooth fade distance at edges (world units)

Volume Blending

When multiple volumes overlap, the system evaluates each volume’s influence using SDF distance functions and blends their effects by priority:

// Global volume — always active, low priority (base layer)
// Entity A: PostProcessVolume with isGlobal=true, priority=0
// effects: [{ type: 'vignette', enabled: true, uniforms: { u_intensity: 0.3 } }]
// Local volume — activates when camera enters the region
// Entity B: PostProcessVolume with isGlobal=false, shape='sphere', size={x:10,y:10}
// priority=1, blendDistance=3
// effects: [{ type: 'blur', enabled: true, uniforms: { u_intensity: 5.0 } }]

Editor Workflow

Add a PostProcessVolume component to a Camera entity in the editor to configure effects visually:

  1. Select the Camera entity in the Hierarchy
  2. Add ComponentPostProcessVolume
  3. Configure volume properties:
    • Is Global — check for scene-wide effects, uncheck for spatial volumes
    • Priority / Weight — control blending order and strength
    • Shape / Size / Blend Distance — define the spatial trigger region (when not global)
  4. Click Add Effect and choose from the built-in effects
  5. Adjust parameters with the sliders

Effects are visible in both the Scene View and the Game View. The component data is saved with the scene file.

Custom Pass

Write a custom fragment shader. The vertex shader uses a fixed full-screen triangle — you only write the fragment shader:

import { Material, PostProcess } from 'esengine';
const invertShader = Material.createShader(
`#version 300 es
precision highp float;
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
v_texCoord = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}`,
`#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
uniform float u_intensity;
out vec4 fragColor;
void main() {
vec4 color = texture(u_texture, v_texCoord);
vec3 inverted = mix(color.rgb, 1.0 - color.rgb, u_intensity);
fragColor = vec4(inverted, color.a);
}`
);
const fx = PostProcess.createStack();
fx.addPass('invert', invertShader);
fx.setUniform('invert', 'u_intensity', 1.0);
PostProcess.bind(cameraEntity, fx);

See Materials & Shaders for more on writing shaders.

Example: Pause Menu Blur

Enable blur and grayscale when the game is paused:

import { PostProcess } from 'esengine';
const pauseFx = PostProcess.createStack();
pauseFx.addPass('blur', PostProcess.createBlur());
pauseFx.addPass('gray', PostProcess.createGrayscale());
pauseFx.setAllPassesEnabled(false);
PostProcess.bind(cameraEntity, pauseFx);
function setPaused(paused: boolean) {
pauseFx.setAllPassesEnabled(paused);
pauseFx.setUniform('blur', 'u_intensity', 4.0);
pauseFx.setUniform('gray', 'u_intensity', 0.6);
}

Example: Different Effects per Camera

// Main camera — bloom + vignette
const mainFx = PostProcess.createStack();
mainFx.addPass('bloom_extract', PostProcess.createBloomExtract());
mainFx.setUniform('bloom_extract', 'u_threshold', 0.4);
for (let i = 0; i < 5; i++) {
mainFx.addPass(`bloom_kawase_${i}`, PostProcess.createBloomKawase(i));
}
mainFx.addPass('bloom_composite', PostProcess.createBloomComposite());
mainFx.setUniform('bloom_composite', 'u_intensity', 1.5);
mainFx.addPass('vignette', PostProcess.createVignette());
mainFx.setUniform('vignette', 'u_intensity', 0.6);
PostProcess.bind(mainCamera, mainFx);
// Minimap camera — grayscale
const minimapFx = PostProcess.createStack();
minimapFx.addPass('gray', PostProcess.createGrayscale());
minimapFx.setUniform('gray', 'u_intensity', 0.5);
PostProcess.bind(minimapCamera, minimapFx);

Lifecycle

MethodDescription
PostProcess.createStack()Create a new effect stack
PostProcess.bind(camera, stack)Bind a stack to a camera
PostProcess.unbind(camera)Unbind the stack from a camera
PostProcess.getStack(camera)Get the bound stack (or null)
PostProcess.setScreenStack(stack)Set the screen-level effect stack (pass null to clear)
PostProcess.screenStackGet the current screen stack
PostProcess.init(width, height)Initialize the pipeline (called automatically)
PostProcess.shutdown()Shut down the pipeline
PostProcess.resize(width, height)Update framebuffer size (called automatically)
PostProcess.begin()Begin capturing the scene for post-processing
PostProcess.end()End capture and execute all effect passes
PostProcess.createBloomExtract()Create bloom brightness extraction shader
PostProcess.createBloomKawase(iteration)Create bloom Kawase blur shader for a given iteration
PostProcess.createBloomComposite()Create bloom composite shader
PostProcess.createBlur()Create a blur effect shader
PostProcess.createVignette()Create a vignette effect shader
PostProcess.createGrayscale()Create a grayscale effect shader
PostProcess.createChromaticAberration()Create a chromatic aberration shader

Next Steps