TSL Post-Processing
Post-processing applies effects to the rendered image. TSL provides both built-in effects and the ability to create custom effects.
Note:
PostProcessingwas renamed toRenderPipelinein r183.PostProcessingstill works as a compatibility wrapper but is deprecated.
Basic Setup
import * as THREE from 'three/webgpu';
import { pass } from 'three/tsl';
// Create renderer
const renderer = new THREE.WebGPURenderer();
await renderer.init();
// Create render pipeline (formerly PostProcessing, renamed in r183)
const renderPipeline = new THREE.RenderPipeline(renderer);
// Create scene pass
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// Output (passthrough)
renderPipeline.outputNode = scenePassColor;
// Render with post-processing
function animate() {
renderPipeline.render(); // Not renderer.render()
}
Built-in Effects
Bloom
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode('output');
// Add bloom
const bloomPass = bloom(scenePassColor);
// Configure
bloomPass.threshold.value = 0.5; // Brightness threshold
bloomPass.strength.value = 1.0; // Bloom intensity
bloomPass.radius.value = 0.5; // Blur radius
// Combine original + bloom
renderPipeline.outputNode = scenePassColor.add(bloomPass);
Gaussian Blur
import { gaussianBlur } from 'three/addons/tsl/display/GaussianBlurNode.js';
const blurred = gaussianBlur(scenePassColor, vec2(2.0)); // Blur strength (sigma)
renderPipeline.outputNode = blurred;
Note (r177): Sigma values were rescaled — double previous sigma values to get the same blur strength. Also,
resolutionwas renamed toresolutionScale(now a scalar, not a Vector2) in r180.
FXAA (Anti-aliasing)
import { fxaa } from 'three/addons/tsl/display/FXAANode.js';
renderPipeline.outputNode = fxaa(scenePassColor);
SMAA (Anti-aliasing)
import { smaa } from 'three/addons/tsl/display/SMAANode.js';
renderPipeline.outputNode = smaa(scenePassColor);
Depth of Field
import { dof } from 'three/addons/tsl/display/DepthOfFieldNode.js';
const scenePass = pass(scene, camera);
const colorNode = scenePass.getTextureNode('output');
const viewZNode = scenePass.getViewZNode();
// dof(colorNode, viewZNode, focusDistance, focalLength, bokehScale)
const dofPass = dof(colorNode, viewZNode, 5.0, 25.0, 1.0);
renderPipeline.outputNode = dofPass;
Note: The DOF API was completely reimplemented in r181. The old
dof(color, depth, { focus, aperture, maxblur })options-object signature no longer works.
Motion Blur
import { motionBlur } from 'three/addons/tsl/display/MotionBlurNode.js';
const scenePass = pass(scene, camera);
const velocityPass = scenePass.getTextureNode('velocity');
const motionBlurPass = motionBlur(scenePassColor, velocityPass);
renderPipeline.outputNode = motionBlurPass;
Screen Space Reflections (SSR)
import { ssr } from 'three/addons/tsl/display/SSRNode.js';
const scenePass = pass(scene, camera);
const colorNode = scenePass.getTextureNode('output');
const depthNode = scenePass.getTextureNode('depth');
const normalNode = scenePass.getTextureNode('normal');
const ssrPass = ssr(colorNode, depthNode, normalNode, camera);
renderPipeline.outputNode = ssrPass;
Ambient Occlusion (SSAO)
import { ao } from 'three/addons/tsl/display/AmbientOcclusionNode.js';
const scenePass = pass(scene, camera);
const depthNode = scenePass.getTextureNode('depth');
const normalNode = scenePass.getTextureNode('normal');
const aoPass = ao(depthNode, normalNode, camera);
renderPipeline.outputNode = scenePassColor.mul(aoPass);
Film Grain
import { film } from 'three/addons/tsl/display/FilmNode.js';
const filmPass = film(scenePassColor, {
intensity: 0.5,
grayscale: false
});
renderPipeline.outputNode = filmPass;
Outline
import { outline } from 'three/addons/tsl/display/OutlineNode.js';
const outlinePass = outline(scene, camera, selectedObjects, {
edgeStrength: 3.0,
edgeGlow: 0.0,
edgeThickness: 1.0,
visibleEdgeColor: new THREE.Color(0xffffff),
hiddenEdgeColor: new THREE.Color(0x190a05)
});
renderPipeline.outputNode = scenePassColor.add(outlinePass);
Chromatic Aberration
import { chromaticAberration } from 'three/addons/tsl/display/ChromaticAberrationNode.js';
const caPass = chromaticAberration(scenePassColor, {
offset: vec2(0.002, 0.002)
});
renderPipeline.outputNode = caPass;
Color Adjustments
Grayscale
import { grayscale } from 'three/tsl';
renderPipeline.outputNode = grayscale(scenePassColor);
Saturation
import { saturation } from 'three/tsl';
// 0 = grayscale, 1 = normal, 2 = oversaturated
renderPipeline.outputNode = saturation(scenePassColor, 1.5);
Hue Shift
import { hue } from 'three/tsl';
// Shift hue by radians
renderPipeline.outputNode = hue(scenePassColor, time.mul(0.5));
Vibrance
import { vibrance } from 'three/tsl';
renderPipeline.outputNode = vibrance(scenePassColor, 0.5);
Posterize
import { posterize } from 'three/tsl';
// Reduce color levels
renderPipeline.outputNode = posterize(scenePassColor, 8);
Sepia
import { sepia } from 'three/addons/tsl/display/SepiaNode.js';
renderPipeline.outputNode = sepia(scenePassColor);
3D LUT
import { lut3D } from 'three/addons/tsl/display/Lut3DNode.js';
const lutTexture = new THREE.Data3DTexture(lutData, size, size, size);
renderPipeline.outputNode = lut3D(scenePassColor, lutTexture, size);
Custom Post-Processing
Basic Custom Effect
import { Fn, screenUV, float, vec4 } from 'three/tsl';
const customEffect = Fn(() => {
const color = scenePassColor.toVar();
// Invert colors
color.rgb.assign(float(1.0).sub(color.rgb));
return color;
});
renderPipeline.outputNode = customEffect();
Vignette Effect
const vignette = Fn(() => {
const color = scenePassColor.toVar();
// Distance from center
const uv = screenUV;
const dist = uv.sub(0.5).length();
// Vignette falloff
const vignette = float(1.0).sub(dist.mul(1.5)).clamp(0, 1);
color.rgb.mulAssign(vignette);
return color;
});
renderPipeline.outputNode = vignette();
CRT/Scanline Effect
import { viewportSharedTexture } from 'three/tsl';
const crtEffect = Fn(() => {
const uv = screenUV;
// Sample scene at offset UVs for RGB separation (chromatic aberration)
const uvR = uv.add(vec2(0.002, 0));
const uvG = uv;
const uvB = uv.sub(vec2(0.002, 0));
// Use viewportSharedTexture to sample at different UV coordinates
const r = viewportSharedTexture(uvR).r;
const g = viewportSharedTexture(uvG).g;
const b = viewportSharedTexture(uvB).b;
const color = vec4(r, g, b, 1.0).toVar();
// Scanlines
const scanline = uv.y.mul(screenSize.y).mul(0.5).sin().mul(0.1).add(0.9);
color.rgb.mulAssign(scanline);
// Vignette
const dist = uv.sub(0.5).length();
color.rgb.mulAssign(float(1.0).sub(dist.mul(0.5)));
return color;
});
// Note: For this effect, apply after scene rendering
renderPipeline.outputNode = crtEffect();
Pixelate Effect
const pixelSize = uniform(8.0);
const pixelate = Fn(() => {
const uv = screenUV;
const pixelUV = uv.mul(screenSize).div(pixelSize).floor().mul(pixelSize).div(screenSize);
return texture(scenePassColor, pixelUV);
});
renderPipeline.outputNode = pixelate();
Edge Detection (Sobel)
const sobelEdge = Fn(() => {
const uv = screenUV;
const texelSize = vec2(1.0).div(screenSize);
// Sample 3x3 kernel
const tl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, -1)))));
const tc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, -1)))));
const tr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, -1)))));
const ml = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 0)))));
const mr = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 0)))));
const bl = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(-1, 1)))));
const bc = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(0, 1)))));
const br = luminance(texture(scenePassColor, uv.add(texelSize.mul(vec2(1, 1)))));
// Sobel operators
const gx = tl.add(ml.mul(2)).add(bl).sub(tr).sub(mr.mul(2)).sub(br);
const gy = tl.add(tc.mul(2)).add(tr).sub(bl).sub(bc.mul(2)).sub(br);
const edge = sqrt(gx.mul(gx).add(gy.mul(gy)));
return vec4(vec3(edge), 1.0);
});
renderPipeline.outputNode = sobelEdge();
Multiple Render Targets (MRT)
Access multiple buffers from the scene pass:
import { mrt, output } from 'three/tsl';
const scenePass = pass(scene, camera);
// Set up MRT
scenePass.setMRT(mrt({
output: output, // Color output
normal: normalView, // View-space normals
depth: depth // Depth buffer
}));
// Access individual targets
const colorTexture = scenePass.getTextureNode('output');
const normalTexture = scenePass.getTextureNode('normal');
const depthTexture = scenePass.getTextureNode('depth');
Selective Bloom with MRT
Bloom only emissive objects by rendering emissive to a separate target:
import { pass, mrt, output, emissive } from 'three/tsl';
import { bloom } from 'three/addons/tsl/display/BloomNode.js';
const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePass = pass(scene, camera);
// Render both color and emissive to separate targets
scenePass.setMRT(mrt({
output: output,
emissive: emissive
}));
// Get the texture nodes
const colorTexture = scenePass.getTextureNode('output');
const emissiveTexture = scenePass.getTextureNode('emissive');
// Apply bloom only to emissive
const bloomPass = bloom(emissiveTexture);
bloomPass.threshold.value = 0.0; // Bloom all emissive
bloomPass.strength.value = 1.5;
bloomPass.radius.value = 0.5;
// Combine: original color + bloomed emissive
renderPipeline.outputNode = colorTexture.add(bloomPass);
This approach prevents non-emissive bright areas (like white surfaces) from blooming.
Chaining Effects
const scenePass = pass(scene, camera);
const color = scenePass.getTextureNode('output');
// Chain multiple effects
let output = color;
// 1. Apply bloom
const bloomPass = bloom(output);
output = output.add(bloomPass.mul(0.5));
// 2. Apply color grading
output = saturation(output, 1.2);
// 3. Apply vignette
const dist = screenUV.sub(0.5).length();
const vignette = float(1.0).sub(dist.mul(0.5));
output = output.mul(vignette);
// 4. Apply FXAA
output = fxaa(output);
renderPipeline.outputNode = output;
Conditional Effects
const effectEnabled = uniform(true);
const conditionalEffect = Fn(() => {
const color = scenePassColor;
return select(effectEnabled, grayscale(color), color);
});
renderPipeline.outputNode = conditionalEffect();
// Toggle at runtime
effectEnabled.value = false;
Transitions
import { transition } from 'three/addons/tsl/display/TransitionNode.js';
const scenePassA = pass(sceneA, camera);
const scenePassB = pass(sceneB, camera);
const transitionProgress = uniform(0);
const transitionPass = transition(
scenePassA.getTextureNode('output'),
scenePassB.getTextureNode('output'),
transitionProgress,
texture(transitionTexture) // Optional transition texture
);
renderPipeline.outputNode = transitionPass;
// Animate transition
function animate() {
transitionProgress.value = Math.sin(time) * 0.5 + 0.5;
renderPipeline.render();
}
Additional Effects (r182+)
These effects were added in recent Three.js releases:
// Volumetric god rays (r183)
import { godrays } from 'three/addons/tsl/display/GodraysNode.js';
const godraysPass = godrays(scenePassColor, depthNode, camera, lightPosition);
// Retro/CRT effect (r183)
import { retroPass } from 'three/addons/tsl/display/RetroNode.js';
// Anamorphic lens flare
import { anamorphic } from 'three/addons/tsl/display/AnamorphicNode.js';
const anamorphicPass = anamorphic(scenePassColor);
// Lens flare
import { lensflare } from 'three/addons/tsl/display/LensflareNode.js';
// Denoising
import { denoise } from 'three/addons/tsl/display/DenoiseNode.js';
// Screen-space global illumination
import { ssgi } from 'three/addons/tsl/display/SSGINode.js';
// Temporal anti-aliasing (replaces FXAA/SMAA for better quality)
import { traa } from 'three/addons/tsl/display/TRAANode.js';
// Alternative blur modes
import { boxBlur } from 'three/addons/tsl/display/BoxBlurNode.js'; // Mobile-friendly
import { hashBlur } from 'three/addons/tsl/display/HashBlurNode.js'; // Single-pass
import { bilateralBlur } from 'three/addons/tsl/display/BilateralBlurNode.js'; // Edge-preserving
// 3D texture sampling (r182)
import { texture3DLoad, texture3DLevel } from 'three/tsl';