Post-Processing

Post-processing effects including bloom, SSAO, depth of field, motion blur, color grading, and custom effect chains.

Post-processing applies screen-space effects after the main scene is rendered. Web Engine provides a complete post-processing pipeline with common effects and support for custom passes.

Built-in Effects#

Bloom

HDR bloom with adjustable intensity, threshold, and radius

SSAO

Screen-space ambient occlusion for contact shadows

Depth of Field

Realistic camera focus with bokeh blur

Motion Blur

Camera and object motion blur

Color Grading

Tone mapping, color correction, and LUTs

Custom Effects

Build your own post-processing passes

Setting Up Post-Processing#

The post-processing system uses Three.js EffectComposer to chain multiple passes:

import {
initPostProcessing,
updatePostProcessing,
renderPostProcessing,
} from '@web-engine-dev/core/engine/rendering';
// Initialize post-processing (once during setup)
initPostProcessing();
// Configure effects
updatePostProcessing({
bloomEnabled: true,
bloomIntensity: 1.5,
bloomThreshold: 0.9,
bloomRadius: 0.5,
toneMappingExposure: 1.0,
});
// Render with post-processing (in game loop)
function gameLoop() {
// Render scene with all post-processing effects
renderPostProcessing();
}

Bloom#

Bloom creates a glow effect around bright areas, simulating how real cameras and eyes perceive intense light:

import { updatePostProcessing } from '@web-engine-dev/core/engine/rendering';
// Enable bloom
updatePostProcessing({
bloomEnabled: true,
bloomIntensity: 1.5, // Glow strength (0-3)
bloomThreshold: 0.9, // Brightness threshold (0-1)
bloomRadius: 0.5, // Blur radius (0-1)
});
// For emissive objects to bloom, increase emissive intensity
material.emissive = new THREE.Color(0xff0000);
material.emissiveIntensity = 2.0; // > 1.0 for bloom
ParameterRangeEffect
intensity0-3Strength of the bloom glow
threshold0-1Minimum brightness to bloom (0.9 = only very bright)
radius0-1Size of the glow radius

Selective Bloom

To make only specific objects bloom, render them to a separate layer and use selective bloom. Alternatively, use high emissive values (greater than 1.0) to ensure they exceed the bloom threshold.

Screen-Space Ambient Occlusion (SSAO)#

SSAO darkens areas where surfaces are close together, adding depth and realism:

import { SSAOPass } from 'three/examples/jsm/postprocessing/SSAOPass.js';
import { getComposer } from '@web-engine-dev/core/engine/rendering';
const composer = getComposer();
if (composer) {
const ssaoPass = new SSAOPass(
scene,
camera,
window.innerWidth,
window.innerHeight
);
// Configure SSAO
ssaoPass.kernelRadius = 16; // Sample radius
ssaoPass.kernelSize = 32; // Number of samples
ssaoPass.minDistance = 0.001; // Min sample distance
ssaoPass.maxDistance = 0.1; // Max sample distance
ssaoPass.output = SSAOPass.OUTPUT.Default;
composer.addPass(ssaoPass);
}
ParameterEffectRecommended
kernelRadiusOcclusion radius in world units8-32
kernelSizeSample count (quality vs performance)16-64
minDistancePrevents self-shadowing0.001-0.005
maxDistanceMaximum occlusion distance0.05-0.2

Depth of Field#

Depth of field simulates camera focus, blurring objects outside the focal range:

import { BokehPass } from 'three/examples/jsm/postprocessing/BokehPass.js';
import { getComposer } from '@web-engine-dev/core/engine/rendering';
const composer = getComposer();
if (composer) {
const bokehPass = new BokehPass(scene, camera, {
focus: 10.0, // Focus distance
aperture: 0.025, // Aperture size (blur amount)
maxblur: 0.01, // Maximum blur amount
width: window.innerWidth,
height: window.innerHeight,
});
composer.addPass(bokehPass);
// Update focus dynamically
function updateFocus(targetDistance) {
bokehPass.uniforms.focus.value = targetDistance;
}
}

Cinematic DOF

For cinematic scenes, use a large aperture (0.05-0.1) and position the focus on your subject. This creates a shallow depth of field that isolates the subject from the background.

Motion Blur#

Motion blur creates streaking for fast-moving objects or camera movement:

import { MotionBlurPass } from 'three/examples/jsm/postprocessing/MotionBlurPass.js';
const motionBlurPass = new MotionBlurPass(scene, camera, {
samples: 16, // Sample count (quality)
intensity: 0.5, // Blur strength
jitter: 1.0, // Sample jitter
velocityFactor: 1.0, // Velocity multiplier
});
composer.addPass(motionBlurPass);

Performance Impact

Motion blur is expensive as it requires velocity buffers and multiple samples. Use sparingly or disable on mobile/low-end devices. Consider using fewer samples (8-12) for better performance.

Color Grading and Tone Mapping#

Color grading adjusts the final image colors for stylistic or artistic effect:

Tone Mapping#

import * as THREE from 'three';
// Set tone mapping on renderer
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
// Available tone mapping algorithms:
// - THREE.NoToneMapping (default, HDR clipping)
// - THREE.LinearToneMapping (simple linear)
// - THREE.ReinhardToneMapping (photographic)
// - THREE.CineonToneMapping (film-like)
// - THREE.ACESFilmicToneMapping (industry standard, recommended)
// Adjust exposure
updatePostProcessing({
toneMappingExposure: 1.5, // Brighter
toneMappingExposure: 0.8, // Darker
});

LUT (Look-Up Table)#

Apply color grading using 3D LUTs for precise color control:

import { LUTPass } from 'three/examples/jsm/postprocessing/LUTPass.js';
import { LUTCubeLoader } from 'three/examples/jsm/loaders/LUTCubeLoader.js';
// Load LUT file (.cube format)
const lutLoader = new LUTCubeLoader();
lutLoader.load('lut/cinematic.cube', (lut) => {
const lutPass = new LUTPass({ lut: lut.texture3D });
lutPass.intensity = 1.0; // 0 = original, 1 = full LUT
composer.addPass(lutPass);
});
// Or create custom color adjustments
const colorCorrectionPass = new ShaderPass({
uniforms: {
tDiffuse: { value: null },
uSaturation: { value: 1.2 },
uContrast: { value: 1.1 },
uBrightness: { value: 1.0 },
},
vertexShader: /* ... */,
fragmentShader: /* ... color correction shader ... */,
});

Custom Post-Processing Effects#

Create your own post-processing effects using ShaderPass:

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
// Custom vignette effect
const vignetteShader = {
uniforms: {
tDiffuse: { value: null },
uIntensity: { value: 0.5 },
uExtent: { value: 0.8 },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform float uIntensity;
uniform float uExtent;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
// Distance from center
vec2 center = vec2(0.5, 0.5);
float dist = distance(vUv, center);
// Vignette falloff
float vignette = smoothstep(uExtent, uExtent - 0.3, dist);
vignette = mix(1.0 - uIntensity, 1.0, vignette);
gl_FragColor = vec4(color.rgb * vignette, color.a);
}
`,
};
const vignettePass = new ShaderPass(vignetteShader);
composer.addPass(vignettePass);

Example: Glitch Effect#

import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js';
const glitchPass = new GlitchPass();
// Trigger glitch intermittently
function triggerGlitch() {
glitchPass.goWild = true;
setTimeout(() => {
glitchPass.goWild = false;
}, 100);
}
composer.addPass(glitchPass);

Example: Film Grain#

import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js';
const filmPass = new FilmPass(
0.35, // Noise intensity
0.5, // Scanline intensity
648, // Scanline count
false // Grayscale
);
composer.addPass(filmPass);

Pass Execution Order#

Post-processing passes execute in the order they are added. The order matters for certain effects:

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { OutputPass } from 'three/examples/jsm/postprocessing/OutputPass.js';
const composer = new EffectComposer(renderer);
// 1. Render Pass (renders the scene)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 2. SSAO (needs depth buffer from render pass)
composer.addPass(ssaoPass);
// 3. Bloom (before tone mapping for HDR)
composer.addPass(bloomPass);
// 4. Depth of Field (after bloom)
composer.addPass(bokehPass);
// 5. Color Grading / LUT
composer.addPass(lutPass);
// 6. Vignette
composer.addPass(vignettePass);
// 7. Output Pass (must be last - handles tone mapping and color space)
const outputPass = new OutputPass();
composer.addPass(outputPass);

Output Pass

Always add an OutputPass as the final pass. It handles tone mapping and color space conversion for proper display.

Performance Optimization#

TechniqueBenefitTrade-off
Reduce resolution2-4x fasterLower quality
Limit pass countFaster per frameFewer effects
Lower sample countsFaster SSAO/DOF/motion blurMore noise/artifacts
Disable on mobileWorks on all devicesNo post-processing
Use simpler effectsBetter performanceLess impressive visuals

Resolution Scaling#

// Render post-processing at half resolution
const renderScale = 0.5;
const width = window.innerWidth * renderScale;
const height = window.innerHeight * renderScale;
composer.setSize(width, height);
// Upscale to screen resolution
// This is 4x faster but slightly blurrier

Best Practices#

Effect Selection#

  • Start with bloom and tone mapping for HDR rendering
  • Add SSAO for enhanced depth perception
  • Use depth of field for cinematic scenes
  • Avoid stacking too many effects (3-5 is usually enough)
  • Test on target devices to ensure acceptable performance

Quality Settings#

  • Provide quality presets (Low, Medium, High, Ultra)
  • Disable expensive effects on mobile (motion blur, DOF)
  • Use lower sample counts on medium settings
  • Scale resolution based on device performance
  • Let users toggle individual effects in settings

Artistic Use#

  • Use bloom sparingly - subtle is usually better
  • Match DOF to your scene's scale and camera settings
  • Use color grading to establish mood and atmosphere
  • Test effects in different lighting conditions
  • Avoid over-processing - maintain visual clarity

Accessibility

Some post-processing effects can cause motion sickness or visual discomfort. Always provide options to disable effects like motion blur, chromatic aberration, and screen shake. Respect the prefers-reduced-motion setting.

Rendering | Web Engine Docs | Web Engine Docs