Skip to main content
skill Claude Code CX Codex Cursor GitHub Copilot

threejs-postprocessing · cloudai-x — Claude Code skill: effects and scene

A skill that adds post-processing pass chains to Three.js: bloom, depth of field, ambient occlusion, and custom screen shaders. Use it when you need visual effects, color grading, or blur on WebGL and WebGPU.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add CloudAI-X/threejs-skills --skill threejs-postprocessing -a claude-code
Open on GitHub

What threejs-postprocessing does

The skill works with EffectComposer, Three.js's built-in tool for effect chains. You add passes in order: RenderPass (render the scene), linear-space effects like UnrealBloomPass or SSAOPass, then SMAAPass for edge smoothing, and finally OutputPass for tone mapping. Composer holds buffers in linear color space; all correction happens once in OutputPass.

The skill includes examples for WebGLRenderer with antialiasing, pixel ratio setup, and full MSAA configuration via WebGLRenderTarget with the samples parameter. Separate examples show selective bloom — lighting only objects on a specific layer, which requires two composers and texture blending.

Requires Three.js r186 and above. WebGPU examples use WebGPURenderer and TSL nodes. Works across all platforms: Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode.

What it helps with

  • Add bloom glow to bright objects
  • Apply depth of field or ambient occlusion
  • Smooth effect edges with SMAA
  • Chain multiple effects into one pipeline
  • Make selective bloom for only marked objects

Example prompts for the agent

  • Add bloom effect to a Three.js scene with strength, radius, and threshold parameters
  • Set up a post-processing chain: render → bloom → SMAA → output
  • Implement selective bloom so only objects on the BLOOM_LAYER glow

How to install threejs-postprocessing

1Skills CLI (Vercel)

npx skills add CloudAI-X/threejs-skills --skill threejs-postprocessing -a claude-code

2Manually: put the files in place

~/.claude/skills/threejs-postprocessing/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/threejs-postprocessing/

From the same set10

The whole set →
threejs-animation CloudAI-X Skill for keyframe, skeletal and morph-target animation in Three.js. Play glTF animations, control bones, blend and crossfade clips on WebGL and WebGPU. Claude Code CX Codex Cursor +1 20,100installs threejs-fundamentals CloudAI-X Sets up Three.js r186 scenes, cameras, renderers, and animation loops for WebGL or WebGPU with TSL. Claude Code CX Codex Cursor +1 17,100installs threejs-shaders CloudAI-X Guides the agent through writing GLSL shaders, custom materials, and TSL nodes for Three.js r186 across WebGL and WebGPU. Claude Code CX Codex Cursor +1 15,400installs threejs-geometry CloudAI-X Teaches your coding agent how to build standard geometries, generate 3D text, and construct custom vertex buffers in three.js. Claude Code CX Codex Cursor +1 14,400installs threejs-materials CloudAI-X Helps choose and configure Three.js materials for meshes, lines, and points. Use it when a scene needs PBR, lighting, transparency, or controlled surface behavior. Claude Code CX Codex Cursor +1 14,400installs threejs-interaction CloudAI-X Teaches your agent how to set up raycasting, camera controls, and object dragging in three.js for WebGL and WebGPU. Claude Code CX Codex Cursor +1 14,100installs threejs-lighting CloudAI-X Three.js r186 lighting skill for WebGL and WebGPU. It guides physical light units, cascaded shadow maps, and RectAreaLight setup. Claude Code CX Codex Cursor +1 13,100installs threejs-loaders CloudAI-X Teaches your AI agent to load glTF models, textures, and HDR maps in three.js r186. Useful when wiring loading progress, color spaces, or WebGPU environments. Claude Code CX Codex Cursor +1 12,700installs

Other versions of threejs-postprocessing

Similar

antislop-human miqdadbadjuber An accessibility skill for AI agents that checks UI contrast, keyboard focus, and control states. Ships with a WCAG AA contrast checker script. Claude Code CX Codex Cursor +1 5,800installs expo-dom expo Runs web code in a webview on native platforms and renders as-is on web. Lets you use web libraries like recharts or react-syntax-highlighter in Expo without rewriting. Claude Code CX Codex Cursor +1 20,800installs antislop-layoutmobile miqdadbadjuber A skill for responsive layouts across phone and desktop widths. It checks breakpoints, reflow, grids, overflow, sizing, tap targets, and navigation. Claude Code CX Codex Cursor +1 5,700installs streamdown vercel Renders Markdown in React components with streaming support for AI agents, Shiki syntax highlighting, Mermaid diagrams, and KaTeX math rendering. Claude Code CX Codex Cursor +1 3,200installs vue-options-api-best-practices github.com/vuejs-ai Guidance for Vue 3 Options API components, covering TypeScript, method context, and lifecycle hook pitfalls. Useful when reviewing or writing Options API code. Claude Code CX Codex Cursor +1 11,600installs frontend-design official anthropics This skill guides intentional visual design for new interfaces and redesigns. Use it when a generic generated UI is not enough. Claude Code Claude.ai 1,025,790installs design-taste-frontend Leonxlnx This skill directs the design of public pages and work screens, then checks the result in a browser through screenshots. Use it when an interface feels generic or unfinished. Claude Code CX Codex Cursor +1 673,100installs prototype Matt Pocock Creates throwaway prototypes for checking state logic or exploring UI before committing the decision to the main codebase. Claude Code 986,201installs

Questions about threejs-postprocessing

How do I install threejs-postprocessing in Claude Code?

Run npx skills add CloudAI-X/threejs-skills --skill threejs-postprocessing -a claude-code, then open ~/.claude/skills/threejs-postprocessing/SKILL.md. For other platforms, replace -a with codex, cursor, gemini-cli, or opencode.

What does threejs-postprocessing do?

It provides examples and patterns for EffectComposer in Three.js: how to build pass chains, correct pass order, MSAA setup, and how to make selective bloom on individual objects.

What is the correct pass order in a composer?

RenderPass → linear-space effects (UnrealBloomPass, SSAOPass, etc.) → SMAAPass (if needed) → OutputPass → FXAAPass (only if using FXAA). OutputPass must come before FXAAPass because FXAA requires sRGB input.

Source text

SKILL.md · MIT
---
name: threejs-postprocessing
description: "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."
---

# Three.js Post-Processing

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

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

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.

```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 { 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:

```javascript
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.

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

```javascript
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

```javascript
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

```javascript
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.

```javascript
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:

```javascript
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.

```javascript
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.

```javascript
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.

```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(); // 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.

```javascript
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:

```javascript
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:

```javascript
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.

© CloudAI-X · MIT · CloudAI-X/threejs-skills

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor