UI Skill

threejs-postprocessing

"Three.js post-processing: composer pass chains, built-in renderer effects, and the WebGPU/TSL node pipeline — bloom, depth of field, ambient occlusion, custom screen effects. Use when adding visual effects, color grading, blur, glow, or screen-space shaders, on WebGL or with WebGPU and TSL."

Path: cloudai-x/threejs-postprocessingStars: 0Author: cloudai-x更新于:2026/05/01 10:05

目录

  1. Quick Start
  2. Pass Order
  3. Composer Setup
  4. Selective Bloom
  5. Depth of Field
  6. Ambient Occlusion
  7. Outline
  8. More Passes
  9. Custom Passes
  10. Renderer Built-In Effects (No Composer)
  11. WebGPU and TSL
  12. Common Mistakes
  13. See Also

Three.js Post-Processing

Targets three.js r186. Examples use WebGLRenderer; the WebGPU and TSL section uses WebGPURenderer.

Quick Start

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";

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);

// A bright emissive sphere gives bloom something to pick up
const mesh = new THREE.Mesh(
  new THREE.SphereGeometry(1, 32, 16),
  new THREE.MeshStandardMaterial({ color: 0x222222, emissive: 0xffffff, emissiveIntensity: 2 }),
);
scene.add(mesh);

// The composer replaces renderer.render(). Render the scene, then effects,
// then OutputPass last: it applies renderer.toneMapping and outputColorSpace once.
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
// bloomPass.strength, .radius and .threshold are live properties; threshold is the luminance above which things glow
composer.addPass(bloomPass);
composer.addPass(new OutputPass());

function animate() {
  composer.render(); // not renderer.render()
}

renderer.setAnimationLoop(animate);

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.

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 { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85));
composer.addPass(new SMAAPass()); // no arguments; the composer sizes it
composer.addPass(new OutputPass());

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:
import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";

const composer = new EffectComposer(
  renderer,
  new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, {
    type: THREE.HalfFloatType,
    samples: 4, // MSAA on the composer's two buffers
  }),
);
composer.setSize(window.innerWidth, window.innerHeight); // scales the buffer by the pixel ratio
  • 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.

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";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";

const BLOOM_LAYER = 1;
const bloomLayer = new THREE.Layers();
bloomLayer.set(BLOOM_LAYER); // test mask: matches any object enabled on this layer

mesh.layers.enable(BLOOM_LAYER); // add the layer; set() would remove the mesh from layer 0

// Paint non-bloomed meshes black, then restore them
const darkMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const materials = new Map();

function darkenNonBloomed(object) {
  if (object.isMesh && bloomLayer.test(object.layers) === false) {
    materials.set(object, object.material);
    object.material = darkMaterial;
  }
}

function restoreMaterial(object) {
  if (materials.has(object)) {
    object.material = materials.get(object);
    materials.delete(object);
  }
}

// Bloom-only composer; with renderToScreen off, its result stays in renderTarget2
const bloomComposer = new EffectComposer(renderer);
bloomComposer.renderToScreen = false;
bloomComposer.addPass(new RenderPass(scene, camera));
bloomComposer.addPass(new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85));

// Final composer: the clean scene plus the bloom texture, mixed by a ShaderPass
const finalComposer = new EffectComposer(renderer);
finalComposer.addPass(new RenderPass(scene, camera));
finalComposer.addPass(
  new ShaderPass(
    new THREE.ShaderMaterial({
      uniforms: {
        baseTexture: { value: null }, // filled from the previous pass
        bloomTexture: { value: bloomComposer.renderTarget2.texture },
      },
      vertexShader: /* glsl */ `
        varying vec2 vUv;
        void main() {
          vUv = uv;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: /* glsl */ `
        uniform sampler2D baseTexture;
        uniform sampler2D bloomTexture;
        varying vec2 vUv;
        void main() {
          gl_FragColor = texture2D(baseTexture, vUv) + texture2D(bloomTexture, vUv);
        }
      `,
    }),
    "baseTexture",
  ),
);
finalComposer.addPass(new OutputPass());

function animate() {
  const background = scene.background;
  scene.background = null; // not a mesh: it would bloom and be added twice
  scene.traverse(darkenNonBloomed); // black scene: only marked meshes bloom
  bloomComposer.render();
  scene.traverse(restoreMaterial);
  scene.background = background;
  finalComposer.render(); // clean scene + bloom to the screen
}

renderer.setAnimationLoop(animate);

Depth of Field

import { BokehPass } from "three/addons/postprocessing/BokehPass.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera)); // BokehPass reads this color image

const bokehPass = new BokehPass(scene, camera, {
  focus: 10.0, // view-space depth that stays sharp, in world units
  aperture: 0.025, // how far out of focus things blur
  maxblur: 0.01, // cap on the blur radius
});
composer.addPass(bokehPass);
composer.addPass(new OutputPass());

// Refocus at runtime: distanceTo matches the view-space depth for objects near the view center
bokehPass.uniforms.focus.value = camera.position.distanceTo(mesh.position);

Ambient Occlusion

import { SSAOPass } from "three/addons/postprocessing/SSAOPass.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";

const composer = new EffectComposer(renderer);

// SSAOPass darkens the previous image, so the scene render must come first
composer.addPass(new RenderPass(scene, camera));

const ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight);
ssaoPass.kernelRadius = 16;
ssaoPass.minDistance = 0.005;
ssaoPass.maxDistance = 0.1;
composer.addPass(ssaoPass);
composer.addPass(new OutputPass());

// Debug views: ssaoPass.output = SSAOPass.OUTPUT.SSAO (or Blur, Depth, Normal); the default shows the shaded scene

Outline

import * as THREE from "three";
import { OutlinePass } from "three/addons/postprocessing/OutlinePass.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));

const outlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), scene, camera);
outlinePass.selectedObjects = [mesh]; // only these are outlined
outlinePass.edgeStrength = 3;
outlinePass.visibleEdgeColor.set(0xffffff);
outlinePass.hiddenEdgeColor.set(0x190a05); // parts hidden behind other objects
composer.addPass(outlinePass);
composer.addPass(new OutputPass());

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.

import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
import { FilmPass } from "three/addons/postprocessing/FilmPass.js";
import { GlitchPass } from "three/addons/postprocessing/GlitchPass.js";
import { HalftonePass } from "three/addons/postprocessing/HalftonePass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
import { VignetteShader } from "three/addons/shaders/VignetteShader.js";

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));

const filmPass = new FilmPass(0.35, false); // grain intensity, grayscale; animates from the composer's delta
composer.addPass(filmPass);

const glitchPass = new GlitchPass();
glitchPass.goWild = true; // glitch every frame; false (the default) glitches at random intervals
composer.addPass(glitchPass);

const halftonePass = new HalftonePass({ shape: 1, radius: 4, scatter: 0, greyscale: false }); // shape: 1 dot, 2 ellipse, 3 line, 4 square, 5 diamond
composer.addPass(halftonePass);

const vignettePass = new ShaderPass(VignetteShader); // uniforms: offset and darkness
vignettePass.uniforms.offset.value = 1.0;
vignettePass.uniforms.darkness.value = 1.0;
composer.addPass(vignettePass);

composer.addPass(new OutputPass());

RenderPixelatedPass renders the scene itself, so it replaces RenderPass instead of following it:

import { RenderPixelatedPass } from "three/addons/postprocessing/RenderPixelatedPass.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";

const composer = new EffectComposer(renderer);
const pixelatedPass = new RenderPixelatedPass(6, scene, camera); // 6 = pixel size
pixelatedPass.normalEdgeStrength = 0.3; // edges where the surface normal changes (creases)
pixelatedPass.depthEdgeStrength = 0.4; // depth-discontinuity edges
composer.addPass(pixelatedPass);
composer.addPass(new OutputPass());

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.

import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { OutputPass } from "three/addons/postprocessing/OutputPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";

const WaveShader = {
  uniforms: {
    tDiffuse: { value: null },
    time: { value: 0 },
    intensity: { value: 1.0 },
  },
  vertexShader: /* glsl */ `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */ `
    uniform sampler2D tDiffuse;
    uniform float time;
    uniform float intensity;
    varying vec2 vUv;
    void main() {
      vec2 uv = vUv;
      uv.x += sin(uv.y * 10.0 + time) * 0.01 * intensity; // wave distortion
      gl_FragColor = texture2D(tDiffuse, uv);
    }
  `,
};

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const wavePass = new ShaderPass(WaveShader);
composer.addPass(wavePass);
composer.addPass(new OutputPass());

// Drive time uniforms from a Timer
const timer = new THREE.Timer();
timer.connect(document); // keeps deltas sane in inactive tabs

function animate() {
  timer.update();
  wavePass.uniforms.time.value = timer.getElapsed();
  composer.render();
}

renderer.setAnimationLoop(animate);

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.

import * as THREE from "three";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";

const renderer = new THREE.WebGLRenderer({ antialias: true, outputBufferType: THREE.HalfFloatType });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
renderer.setEffects([bloomPass]);

function animate() {
  renderer.render(scene, camera); // bloom runs on the internal HDR buffers after the scene render
}

renderer.setAnimationLoop(animate);

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.

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(); // before any other use of the renderer
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111); // the WebGPU canvas is transparent by default
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
scene.add(new THREE.Mesh(new THREE.SphereGeometry(1, 32, 16), new THREE.MeshBasicNodeMaterial({ color: 0xffffff })));
const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode("output");

// bloom() returns only the glow, so add it to the scene color
renderPipeline.outputNode = scenePassColor.add(bloom(scenePassColor, 1.5, 0.4, 0.85));

function animate() {
  renderPipeline.render(); // instead of renderer.render()
}

renderer.setAnimationLoop(animate);

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.

import * as THREE from "three/webgpu";
import { pass, renderOutput } from "three/tsl";
import { dof } from "three/addons/tsl/display/DepthOfFieldNode.js";
import { fxaa } from "three/addons/tsl/display/FXAANode.js";

const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePass = pass(scene, camera);
const scenePassColor = scenePass.getTextureNode("output");
// A filter replaces the image: node, viewZ, focus distance, focal length, bokeh scale
const dofNode = dof(scenePassColor, scenePass.getViewZNode(), 4.5, 1.2, 2.5);

// FXAA runs in sRGB: disable the automatic output transform, then convert the DOF result before FXAA
renderPipeline.outputColorTransform = false;
renderPipeline.outputNode = fxaa(renderOutput(dofNode));

// After the first frame, a changed outputNode or outputColorTransform only takes effect with:
renderPipeline.needsUpdate = true;

ao() needs depth and view-space normals from a separate, single-sampled pre-pass, and its AO lives in the red channel:

import * as THREE from "three/webgpu";
import { pass, mrt, normalView, screenUV, builtinAOContext } from "three/tsl";
import { ao } from "three/addons/tsl/display/GTAONode.js";

const renderPipeline = new THREE.RenderPipeline(renderer);
// Pre-pass with view-space normals in the output MRT attachment
const prePass = pass(scene, camera, { samples: 0 }); // ao() cannot read a multisampled depth texture
prePass.setMRT(mrt({ output: normalView }));

const aoPass = ao(prePass.getTextureNode("depth"), prePass.getTextureNode(), camera);

// Apply the AO to a normal scene pass
const scenePass = pass(scene, camera);
scenePass.contextNode = builtinAOContext(aoPass.getTextureNode().sample(screenUV).r);
renderPipeline.outputNode = scenePass;

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:

import * as THREE from "three/webgpu";
import { pass, screenUV, sin, time, vec2 } from "three/tsl";

const renderPipeline = new THREE.RenderPipeline(renderer);
const scenePassColor = pass(scene, camera).getTextureNode("output");
const offset = vec2(sin(screenUV.y.mul(10).add(time)).mul(0.01), 0);
renderPipeline.outputNode = scenePassColor.sample(screenUV.add(offset));

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.