threejs-postprocessing
Three.js post-processing - EffectComposer, bloom, DOF, screen effects. Use when adding visual effects, color grading, blur, glow, or creating custom screen-space shaders.
By cloudai-x · 8,447 installs
npx skills add cloudai-x/threejs-skills --skill threejs-postprocessing
Source repository · Upstream listing
Three.js Post Processing
Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.
Quick Start
Pass Order
The composer's buffers hold linear colors; tone mapping and the sRGB conversion happen once, in OutputPass , using the renderer's settings. Build chains in this order: RenderPass → linear space effects (bloom, depth of field, SSAO, outline, ...) → SMAAPass , after the effects whose edges it should smooth → OutputPass → FXAAPass ( three/addons/postprocessing/FXAAPass.js ) only when FXAA is used, because FXAA needs sRGB input. Without FXAA, OutputPass is the last pass.
SSAOPass darkens the image produced by the previous pass, so a RenderPass must come before it. RenderPixelatedPass renders the scene itself and replaces RenderPass . pass.enabled = false skips a pass without rebuilding the chain.
Composer Setup
The composer copies the renderer's pixel ratio at construction. Call renderer.setPixelRatio() before constructing it, or composer.setPixelRatio() afterwards.
The composer's buffers are not multisampled; antialias: true on the renderer does not reach them. For MSAA on the scene render and the composer's buffers, pass your own buffer with samples (CSS pixel size) and size the composer once:
On resize, call camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); composer.setSize(w, h); — the composer resizes its buffers and every pass, pixel ratio included. Never resize a pass by hand.
composer.render() drives time based passes (FilmPass) from its own internal timer; pass a delta in seconds only if you manage time yourself.
The composer assigns renderToScreen to the last enabled pass every frame. Set composer.renderToScreen = false only to keep the result off screen, as selective bloom does below.
Selective Bloom
Bloom only meshes marked with a layer: paint everything else black for a bloom only composer that renders off screen, then mix its texture into a final composer that renders the clean scene. scene.background is not a mesh: set it to null around bloomComposer.render() , or it is added to the final image a second time; on resize, call setSize on both composers.
Depth of Field
Ambient Occlusion
Outline
More Passes
Film grain, glitch, halftone and vignette are drop in filters for the middle of the chain. ColorCorrectionShader works like the vignette: a ShaderPass with powRGB / mulRGB / addRGB uniform vectors. Its default powRGB of (2, 2, 2) darkens the image; start from (1, 1, 1) for neutral.
RenderPixelatedPass renders the scene itself, so it replaces RenderPass instead of following it:
Custom Passes
Wrap any ShaderMaterial style shader object in a ShaderPass . The tDiffuse uniform receives the previous pass output. Keep custom effects before OutputPass while they work in linear space; place one after it only if it needs sRGB input, like FXAA.
Renderer Built In Effects (No Composer)
renderer.setEffects() runs ordinary passes on internal HDR buffers after a plain renderer.render() — no composer, and tone mapping and output color space are applied automatically after the effects. It requires outputBufferType: THREE.HalfFloatType (or FloatType ), takes no OutputPass or FXAAPass , anti aliases through antialias: true , and resizes with renderer.setSize() . Effects receive no delta time here: FilmPass's time uniform becomes NaN and its grain is lost, so run time driven passes through EffectComposer.
WebGPU and TSL
Under WebGPURenderer, post processing is a TSL node graph on a RenderPipeline , not a pass chain: render the scene with pass() , build effects from three/addons/tsl/display/ .js , and assign the final node to renderPipeline.outputNode . pass() uses the renderer's MSAA samples, so antialias: true works here, except for the ao() pre pass, which must be pass(scene, camera, { samples: 0 }) , and traa , which needs a renderer without antialias ; pass() also follows renderer.setSize() , so the pipeline needs no resize call of its own.
Filters return the finished image and replace the output node: dof , film , rgbShift , chromaticAberration , afterImage , dotScreen , lut3D and smaa (linear input) take the scene color node, pixelationPass(scene, camera, 6) renders the scene itself, and traa needs a velocity MRT and a renderer without antialias . Like bloom , outline(scene, camera, { selectedObjects }) returns only edge masks ( .visibleEdge , .hiddenEdge ) that you color and add to the scene color, and ssao(depthNode, normalNode, camera) returns only the occlusion factor in .r , which multiplies the scene color.
ao() needs depth and view space normals from a separate, single sampled pre pass, and its AO lives in the red channel:
A custom effect is plain TSL over the scene texture instead of a GLSL ShaderPass ; this is the wave from Custom Passes, animated by the time node:
Selective bloom needs no layer trick here: add emissive (from three/tsl ) to the pass MRT with scenePass.setMRT(mrt({ output, emissive })) and bloom only that attachment: renderPipeline.outputNode = scenePassColor.add(bloom(scenePass.getTextureNode("emissive"))) .
Common Mistakes
three/examples/jsm/postprocessing/EffectComposer.js → three/addons/postprocessing/EffectComposer.js : the examples/jsm specifier does not resolve under the import map.
composer.addPass(new ShaderPass(GammaCorrectionShader)) → composer.addPass(new OutputPass()) : GammaCorrectionShader only applies the sRGB transfer function, while OutputPass applies renderer tone mapping, exposure and output color space and ends every chain; only passes that need sRGB input, such as FXAAPass, follow it.
composer.addPass(bloomPass); composer.render(); → composer.addPass(bloomPass); composer.addPass(new OutputPass()); composer.render(); : the composer's buffers are linear, and without OutputPass tone mapping and the sRGB conversion are skipped or applied unevenly.
composer.addPass(fxaaPass); composer.addPass(outputPass); → composer.addPass(outputPass); composer.addPass(fxaaPass); : FXAA needs sRGB input so it follows OutputPass, while SMAA works in linear space and must run before it.
new ShaderPass(FXAAShader) → new FXAAPass() : FXAAPass sets its own resolution uniform from the composer size (pixel ratio included); a hand set value is wrong on HiDPI screens.
new FilmPass(0.35, 0.5, 648, false) → new FilmPass(0.35, false) : the signature is (intensity, grayscale); the scanline arguments do not exist.
new HalftonePass(width, height, params) → new HalftonePass(params) : the width and height arguments were removed in r175; options go in the single params object.
new SMAAPass(width, height) → new SMAAPass() : the size arguments were removed in r175; the composer sizes the pass.
new UnrealBloomPass(new THREE.Vector2(window.innerWidth / 2, window.innerHeight / 2), 1.5, 0.4, 0.85) → new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85) : addPass immediately resizes the pass to the composer size, undoing the half resolution trick.
bloomPass.resolution.set(width, height) → composer.setSize(width, height) : UnrealBloomPass reads its resolution only in the constructor; the composer resizes the pass and its mips.
bloomPass.renderToScreen = false → bloomComposer.renderToScreen = false : the composer reassigns renderToScreen on every enabled pass each frame, so only the composer level switch keeps the result off screen.
requestAnimationFrame(animate) → renderer.setAnimationLoop(animate) : both renderers' docs advise it over a manual loop, and it hands frames to the WebXR session when one starts.
clock.getElapsedTime() → timer.getElapsed() : Clock is deprecated in r183; create a THREE.Timer, call timer.connect(document) once and timer.update() before reading it each frame.
composer.render(); renderer.render(scene, camera); → bloomComposer.render(); finalComposer.render(); : the second renderer.render clears the canvas and erases the composer output; selective bloom mixes the bloom texture in through a second composer.
const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setEffects([bloomPass]); → const renderer = new THREE.WebGLRenderer({ antialias: true, outputBufferType: THREE.HalfFloatType }); renderer.setEffects([bloomPass]); : setEffects() requires a HalfFloatType or FloatType output buffer; with the default it logs an error and applies no effects.
renderer.setEffects([bloomPass, new OutputPass(), new FXAAPass()]) → renderer.setEffects([bloomPass]) : the built in path applies tone mapping and color space once, after the effects, so an OutputPass in the list applies them twice (it warns) and an FXAAPass without one gets linear input; anti alias with antialias: true instead.
import { bloom } from "three/tsl" → import { bloom } from "three/addons/tsl/display/BloomNode.js" : three/tsl exports pass, mrt and renderOutput; the effect nodes live in the addons.
new THREE.PostProcessing(renderer) → new THREE.RenderPipeline(renderer) : renamed from PostProcessing in r183; the old name survives only as a deprecated alias that warns.
renderPipeline.outputNode = bloom(scenePassColor) → renderPipeline.outputNode = scenePassColor.add(bloom(scenePassColor)) : bloom() returns only the glow, so on its own the scene is never drawn.
renderPipeline.outputNode = fxaa(scenePassColor) → renderPipeline.outputColorTransform = false; renderPipeline.outputNode = fxaa(renderOutput(scenePass)); : FXAA needs sRGB input, but the default outputColorTransform tone maps and converts after it.
renderPipeline.outputNode = scenePassColor.add(dof(scenePassColor, scenePass.getViewZNode())) → renderPipeline.outputNode = dof(scenePassColor, scenePass.getViewZNode()) : dof returns the finished image, so adding the scene color draws the scene twice.
await renderPipeline.renderAsync() → renderPipeline.render() : renderAsync is deprecated in r181; call await renderer.init() once at setup and render synchronously.
aoPass.distanceFallOff.value = 2 → aoPass.radius.value = 0.2 : distanceExponent and distanceFallOff are deprecated in r186 and no longer change the AO, so tune radius and scale instead.
new EffectComposer(webgpuRenderer) → new THREE.RenderPipeline(webgpuRenderer) : EffectComposer and its passes run only under WebGLRenderer; WebGPURenderer effects are TSL nodes on a RenderPipeline.
See Also
threejs fundamentals — renderer creation, sizing, color spaces and the animation loop.
threejs shaders — GLSL ShaderMaterial authoring and the TSL node language for custom effects.
threejs textures — render targets and using a post processed image as a texture.