# Three.js Post-Processing - Detailed Guide > This file contains the detailed procedure and reference material extracted from `SKILL.md` for focused loading. The root skill defines activation, examples, safety constraints, and limitations. Examples target Three.js r183 unless stated otherwise. Pin core and addons to the same release; WebGL and WebGPU pipelines use different APIs. Assume an existing renderer, scene, camera and animation setup. Profile on target devices and dispose composers, passes, render targets and event listeners when replacing a pipeline. These snippets are not browser-tested here. ## Quick Start ```javascript import * as THREE from "three"; import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; // Setup composer const composer = new EffectComposer(renderer); // Render scene const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); // Add bloom const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, // strength 0.4, // radius 0.85, // threshold ); composer.addPass(bloomPass); composer.addPass(new OutputPass()); // Tone mapping and output color conversion // Animation loop - use composer instead of renderer function animate() { requestAnimationFrame(animate); composer.render(); // NOT renderer.render() } ``` ## EffectComposer Setup ```javascript import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; const composer = new EffectComposer(renderer); // First pass: render scene const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); // Add more passes... composer.addPass(effectPass); // Last pass should render to screen effectPass.renderToScreen = true; // Default for last pass // Handle resize function onResize() { const width = window.innerWidth; const height = window.innerHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); composer.setSize(width, height); } ``` ## Common Effects ### Bloom (Glow) ```javascript import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, // strength - intensity of glow 0.4, // radius - spread of glow 0.85, // threshold - brightness threshold ); composer.addPass(bloomPass); // Adjust at runtime bloomPass.strength = 2.0; bloomPass.threshold = 0.5; bloomPass.radius = 0.8; ``` ### Selective Bloom Apply bloom only to specific objects. ```javascript import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; // Layer setup const BLOOM_LAYER = 1; const bloomLayer = new THREE.Layers(); bloomLayer.set(BLOOM_LAYER); // Mark objects to bloom glowingMesh.layers.enable(BLOOM_LAYER); // Dark material for non-blooming objects const darkMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 }); const materials = {}; function darkenNonBloomed(obj) { if (obj.isMesh && !bloomLayer.test(obj.layers)) { materials[obj.uuid] = obj.material; obj.material = darkMaterial; } } function restoreMaterial(obj) { if (materials[obj.uuid]) { obj.material = materials[obj.uuid]; delete materials[obj.uuid]; } } // Use a separate offscreen bloom composer (RenderPass + UnrealBloomPass). const bloomComposer = new EffectComposer(renderer); bloomComposer.renderToScreen = false; bloomComposer.addPass(new RenderPass(scene, camera)); bloomComposer.addPass(new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 1.5, 0.4, 0.85)); const mixPass = new ShaderPass({ uniforms: { tDiffuse: { value: null }, bloomTexture: { value: null } }, vertexShader: `varying vec2 vUv; void main(){vUv=uv; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`, fragmentShader: `uniform sampler2D tDiffuse; uniform sampler2D bloomTexture; varying vec2 vUv; void main(){gl_FragColor=texture2D(tDiffuse,vUv)+texture2D(bloomTexture,vUv);}`, }); const finalComposer = new EffectComposer(renderer); finalComposer.addPass(new RenderPass(scene, camera)); finalComposer.addPass(mixPass); finalComposer.addPass(new OutputPass()); function render() { scene.traverse(darkenNonBloomed); try { bloomComposer.render(); } finally { scene.traverse(restoreMaterial); } mixPass.uniforms.bloomTexture.value = bloomComposer.readBuffer.texture; finalComposer.render(); } // Import EffectComposer, RenderPass and OutputPass as in Quick Start. // Resize both composers. A direct renderer.render() here would overwrite bloom. ``` ### FXAA (Anti-Aliasing) ```javascript import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; import { FXAAShader } from "three/addons/shaders/FXAAShader.js"; const fxaaPass = new ShaderPass(FXAAShader); fxaaPass.material.uniforms["resolution"].value.set( 1 / (window.innerWidth * renderer.getPixelRatio()), 1 / (window.innerHeight * renderer.getPixelRatio()), ); composer.addPass(fxaaPass); // Update on resize function onResize() { fxaaPass.material.uniforms["resolution"].value.set( 1 / (window.innerWidth * renderer.getPixelRatio()), 1 / (window.innerHeight * renderer.getPixelRatio()), ); } ``` ### SMAA (Better Anti-Aliasing) ```javascript import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js"; const smaaPass = new SMAAPass(); // Composer supplies size; place before OutputPass. composer.addPass(smaaPass); ``` ### SSAO (Ambient Occlusion) ```javascript import { SSAOPass } from "three/addons/postprocessing/SSAOPass.js"; const ssaoPass = new SSAOPass( scene, camera, window.innerWidth, window.innerHeight, ); ssaoPass.kernelRadius = 16; ssaoPass.minDistance = 0.005; ssaoPass.maxDistance = 0.1; composer.addPass(ssaoPass); // Output modes ssaoPass.output = SSAOPass.OUTPUT.Default; // SSAOPass.OUTPUT.Default - Final composited output // SSAOPass.OUTPUT.SSAO - Just the AO // SSAOPass.OUTPUT.Blur - Blurred AO // SSAOPass.OUTPUT.Depth - Depth buffer // SSAOPass.OUTPUT.Normal - Normal buffer ``` ### Depth of Field (DOF) ```javascript import { BokehPass } from "three/addons/postprocessing/BokehPass.js"; const bokehPass = new BokehPass(scene, camera, { focus: 10.0, // Focus distance aperture: 0.025, // Aperture (smaller = more DOF) maxblur: 0.01, // Max blur amount }); composer.addPass(bokehPass); // Update focus dynamically bokehPass.uniforms["focus"].value = distanceToTarget; ``` ### Film Grain ```javascript import { FilmPass } from "three/addons/postprocessing/FilmPass.js"; const filmPass = new FilmPass(0.35, false); // r183: intensity, grayscale composer.addPass(filmPass); ``` ### Vignette ```javascript import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; import { VignetteShader } from "three/addons/shaders/VignetteShader.js"; const vignettePass = new ShaderPass(VignetteShader); vignettePass.uniforms["offset"].value = 1.0; // Vignette size vignettePass.uniforms["darkness"].value = 1.0; // Vignette intensity composer.addPass(vignettePass); ``` ### Color Correction ```javascript import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; import { ColorCorrectionShader } from "three/addons/shaders/ColorCorrectionShader.js"; const colorPass = new ShaderPass(ColorCorrectionShader); colorPass.uniforms["powRGB"].value = new THREE.Vector3(1.2, 1.2, 1.2); // Power colorPass.uniforms["mulRGB"].value = new THREE.Vector3(1.0, 1.0, 1.0); // Multiply composer.addPass(colorPass); ``` ### Output Color Conversion Use `OutputPass` once after linear-space effects for tone mapping and output conversion. Do not stack a legacy GammaCorrectionShader with OutputPass. FXAA operates on display-referred color, so place it after OutputPass; SMAA operates before OutputPass in r183. ### Pixelation ```javascript import { RenderPixelatedPass } from "three/addons/postprocessing/RenderPixelatedPass.js"; const pixelPass = new RenderPixelatedPass(6, scene, camera); // 6 = pixel size composer.addPass(pixelPass); ``` ### Glitch Effect ```javascript import { GlitchPass } from "three/addons/postprocessing/GlitchPass.js"; const glitchPass = new GlitchPass(); glitchPass.goWild = false; // Continuous glitching composer.addPass(glitchPass); ``` ### Halftone ```javascript import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js"; const halftonePass = new HalftonePass(window.innerWidth, window.innerHeight, { shape: 1, // 1 = dot, 2 = ellipse, 3 = line, 4 = square radius: 4, // Dot size rotateR: Math.PI / 12, rotateB: (Math.PI / 12) * 2, rotateG: (Math.PI / 12) * 3, scatter: 0, blending: 1, blendingMode: 1, greyscale: false, }); composer.addPass(halftonePass); ``` ### Outline ```javascript import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js"; const outlinePass = new OutlinePass( new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera, ); outlinePass.edgeStrength = 3; outlinePass.edgeGlow = 0; outlinePass.edgeThickness = 1; outlinePass.pulsePeriod = 0; outlinePass.visibleEdgeColor.set(0xffffff); outlinePass.hiddenEdgeColor.set(0x190a05); // Select objects to outline outlinePass.selectedObjects = [mesh1, mesh2]; composer.addPass(outlinePass); ``` ## Custom ShaderPass Create your own post-processing effects. ```javascript import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; const CustomShader = { uniforms: { tDiffuse: { value: null }, // Required: input texture time: { value: 0 }, intensity: { value: 1.0 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform float time; uniform float intensity; varying vec2 vUv; void main() { vec2 uv = vUv; // Wave distortion uv.x += sin(uv.y * 10.0 + time) * 0.01 * intensity; vec4 color = texture2D(tDiffuse, uv); gl_FragColor = color; } `, }; const customPass = new ShaderPass(CustomShader); composer.addPass(customPass); // Update in animation loop customPass.uniforms.time.value = clock.getElapsedTime(); ``` ### Invert Colors Shader ```javascript const InvertShader = { uniforms: { tDiffuse: { value: null }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; varying vec2 vUv; void main() { vec4 color = texture2D(tDiffuse, vUv); gl_FragColor = vec4(1.0 - color.rgb, color.a); } `, }; ``` ### Chromatic Aberration ```javascript const ChromaticAberrationShader = { uniforms: { tDiffuse: { value: null }, amount: { value: 0.005 }, }, vertexShader: ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform sampler2D tDiffuse; uniform float amount; varying vec2 vUv; void main() { vec2 dir = vUv - 0.5; float dist = length(dir); float r = texture2D(tDiffuse, vUv - dir * amount * dist).r; float g = texture2D(tDiffuse, vUv).g; float b = texture2D(tDiffuse, vUv + dir * amount * dist).b; gl_FragColor = vec4(r, g, b, 1.0); } `, }; ``` ## Combining Multiple Effects ```javascript import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js"; import { FXAAShader } from "three/addons/shaders/FXAAShader.js"; import { VignetteShader } from "three/addons/shaders/VignetteShader.js"; import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; const composer = new EffectComposer(renderer); // 1. Render scene composer.addPass(new RenderPass(scene, camera)); // 2. Bloom const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, 0.4, 0.85, ); composer.addPass(bloomPass); // 3. Vignette const vignettePass = new ShaderPass(VignetteShader); vignettePass.uniforms["offset"].value = 0.95; vignettePass.uniforms["darkness"].value = 1.0; composer.addPass(vignettePass); // 4. Tone mapping and output color conversion composer.addPass(new OutputPass()); // 5. Anti-aliasing (always last before output) const fxaaPass = new ShaderPass(FXAAShader); fxaaPass.uniforms["resolution"].value.set( 1 / (window.innerWidth * renderer.getPixelRatio()), 1 / (window.innerHeight * renderer.getPixelRatio()), ); composer.addPass(fxaaPass); ``` ## Render to Texture ```javascript // Create render target const renderTarget = new THREE.WebGLRenderTarget(512, 512); // Render scene to target renderer.setRenderTarget(renderTarget); renderer.render(scene, camera); renderer.setRenderTarget(null); // Use texture const texture = renderTarget.texture; otherMaterial.map = texture; ``` ## Multi-Pass Rendering Two composers that both render to screen do not automatically blend their images; RenderPass may clear, and transparent pixels do not define the desired composition. Render each branch to its own target, then combine its textures in a final fullscreen shader with an explicit alpha/additive rule and one output conversion. The selective bloom example above is a concrete two-branch additive composition. Preserve occlusion/depth deliberately when scenes need to interact. ## WebGPU Post-Processing (Three.js r183) The WebGPU renderer uses a node-based `RenderPipeline` class instead of `EffectComposer`. Note that `EffectComposer` is **WebGL-only**. ```javascript import * as THREE from "three/webgpu"; import { pass } from "three/tsl"; import { bloom } from "three/addons/tsl/display/BloomNode.js"; const renderer = new THREE.WebGPURenderer({ antialias: true }); await renderer.init(); // Create post-processing const postProcessing = new THREE.RenderPipeline(renderer); // Scene pass const scenePass = pass(scene, camera); // Add bloom const sceneColor = scenePass.getTextureNode("output"); const bloomPass = bloom(sceneColor, 0.5, 0.4, 0.85); // Set output postProcessing.outputNode = sceneColor.add(bloomPass); // Render renderer.setAnimationLoop(() => { postProcessing.render(); }); ``` ### Key Differences from EffectComposer | EffectComposer (WebGL) | RenderPipeline (WebGPU) | | ------------------------------- | -------------------------------- | | `addPass(new RenderPass(...))` | `pass(scene, camera)` | | `addPass(new UnrealBloomPass)` | `bloom(scenePass, ...)` | | `composer.render()` | `postProcessing.render()` | | Chain of passes | Node graph with `outputNode` | | GLSL shader passes | TSL node-based effects | ## Performance Tips 1. **Limit passes**: Each pass adds a full-screen render 2. **Lower resolution**: Use smaller render targets for blur passes 3. **Disable unused effects**: Toggle passes on/off 4. **Use FXAA over MSAA**: Less expensive anti-aliasing 5. **Profile with DevTools**: Check GPU usage ```javascript // Disable pass bloomPass.enabled = false; // Reduce the whole composer's resolution for a measured performance tradeoff. // Constructor dimensions on a pass can be overwritten when a composer resizes it. composer.setPixelRatio(Math.min(renderer.getPixelRatio(), 1)); // Use measured GPU time and a user quality setting, not UA detection alone. expensivePass.enabled = qualitySetting === 'high'; ``` ## Handle Resize ```javascript function onWindowResize() { const width = window.innerWidth; const height = window.innerHeight; const pixelRatio = renderer.getPixelRatio(); camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); composer.setSize(width, height); // Update pass-specific resolutions if (fxaaPass) { fxaaPass.material.uniforms["resolution"].value.set( 1 / (width * pixelRatio), 1 / (height * pixelRatio), ); } // composer.setSize() propagates sizes to its passes, including bloom. } window.addEventListener("resize", onWindowResize); ``` ## See Also - `threejs-shaders` - Custom shader development - `threejs-textures` - Render targets - `threejs-fundamentals` - Renderer setup