Back to Creative & Visual

threejs-postprocessing

three.jspost-processingwebglvisual effectsbloomdepth of fieldeffectcomposer3d graphics
2.4k🕒 2026-01-19Source ↗

Install this skill

npx skills add cloudai-x/threejs-skills

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

The Three.js postprocessing skill manages the visual pipeline applied to rendered scenes after the initial geometry pass. By implementing an EffectComposer, developers transition from standard renderer output to a multi-stage process where image-space shaders generate visual enhancements. This involves chaining various modules—such as color grading, bloom, and spatial filters—directly into the frame buffer. Because the composer replaces the standard renderer's frame output, managing render targets and pass sequences is necessary for visual fidelity. This system enables aesthetic depth, such as simulated camera lens effects or complex lighting accumulation, which are otherwise difficult to achieve with basic material shaders. It provides a modular architecture for stacking passes like Bokeh, SSAO, and anti-aliasing while maintaining performance through deliberate control over the render loop and resolution settings.

When to Use This Skill

  • Adding glowing neon highlights to interactive UI elements or objects
  • Improving edge quality on low-resolution renders using SMAA
  • Creating realistic depth perception in virtual photography scenes
  • Applying global ambient occlusion to flat-lit 3D environments

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • add bloom to my three.js scene
  • how to implement effectcomposer for post-processing
  • fix aliasing in three.js with smaa
  • create depth of field effect in three.js
  • setup ambient occlusion in threejs

Pro Tips

  • 💡Chain multiple passes: Leverage the `EffectComposer` to combine various passes (e.g., `RenderPass`, `UnrealBloomPass`, `SAOPass`) for layered visual effects, optimizing their order for desired output.
  • 💡Optimize performance: Be mindful of the number and complexity of your post-processing passes, as they can be GPU intensive. Consider techniques like downsampling for passes that don't require full resolution, or using conditional rendering based on user device capabilities.
  • 💡Custom shader integration: Use `ShaderPass` to easily integrate your own GLSL shaders into the post-processing pipeline, allowing for limitless creative control over visual effects beyond the standard Three.js library.

What this skill does

  • Execute post-rendering filter chains via EffectComposer
  • Apply cinematic bloom and physical light bleeding
  • Perform screen-space ambient occlusion for depth and contact shadows
  • Integrate anti-aliasing techniques like FXAA and SMAA
  • Simulate lens-based depth of field with Bokeh effects

When not to use it

  • Simple projects where standard light/shadow materials provide enough visual impact
  • Performance-critical mobile applications where fill-rate overhead is a strict bottleneck

Example workflow

  1. Instantiate the EffectComposer using your existing WebGLRenderer
  2. Add a RenderPass to capture the initial scene state
  3. Append aesthetic passes like UnrealBloomPass or BokehPass to the composer
  4. Set the final pass to renderToScreen true
  5. Replace the standard renderer.render() loop with composer.render()
  6. Synchronize composer resolution with window resize events

Prerequisites

  • Basic Three.js scene setup
  • WebGLRenderer instance
  • Understanding of the animation loop

Pitfalls & limitations

  • !Forgetting to switch the render loop from renderer.render to composer.render
  • !Failing to update composer size during browser window resize events
  • !Over-stacking passes which exponentially increases frame time overhead

FAQ

Why is my scene black after adding the composer?
Ensure you are calling composer.render() instead of renderer.render() in your animation loop. Also, verify that the last pass in your chain has renderToScreen set to true.
Can I apply bloom only to specific objects?
Yes, use a Layer-based approach where you render the scene twice: once for the bloom pass excluding non-glowing objects, and once for the original scene.
Is it better to use FXAA or SMAA?
FXAA is faster and lower-impact, making it ideal for mobile devices. SMAA provides significantly higher image quality but requires more GPU resources.

How it compares

Doing this manually requires complex manual frame buffer management and shader writing, whereas this skill provides a pre-architected framework to plug-and-play production-ready effects.

Source & trust

2.4k stars🕒 Updated 2026-01-19
📄 Full skill instructions — original source: cloudai-x/threejs-skills
# Three.js Post-Processing

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

// 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);

// Animation loop - use composer instead of renderer
function animate() {
requestAnimationFrame(animate);
composer.render(); // NOT renderer.render()
}


## EffectComposer Setup

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)

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.

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];
}
}

// Custom render loop
function render() {
// Render bloom pass
scene.traverse(darkenNonBloomed);
composer.render();
scene.traverse(restoreMaterial);

// Render final scene over bloom
renderer.render(scene, camera);
}


### FXAA (Anti-Aliasing)

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,
1 / window.innerHeight,
);

composer.addPass(fxaaPass);

// Update on resize
function onResize() {
fxaaPass.material.uniforms["resolution"].value.set(
1 / window.innerWidth,
1 / window.innerHeight,
);
}


### SMAA (Better Anti-Aliasing)

import { SMAAPass } from "three/addons/postprocessing/SMAAPass.js";

const smaaPass = new SMAAPass(
window.innerWidth * renderer.getPixelRatio(),
window.innerHeight * renderer.getPixelRatio(),
);

composer.addPass(smaaPass);


### SSAO (Ambient Occlusion)

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)

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

import { FilmPass } from "three/addons/postprocessing/FilmPass.js";

const filmPass = new FilmPass(
0.35, // noise intensity
0.5, // scanline intensity
648, // scanline count
false, // grayscale
);

composer.addPass(filmPass);


### Vignette

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

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


### Gamma Correction

import { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.js";

const gammaPass = new ShaderPass(GammaCorrectionShader);
composer.addPass(gammaPass);


### Pixelation

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

const pixelPass = new RenderPixelatedPass(6, scene, camera); // 6 = pixel size

composer.addPass(pixelPass);


### Glitch Effect

import { GlitchPass } from "three/addons/postprocessing/GlitchPass.js";

const glitchPass = new GlitchPass();
glitchPass.goWild = false; // Continuous glitching

composer.addPass(glitchPass);


### Halftone

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

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.

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

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

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

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 { GammaCorrectionShader } from "three/addons/shaders/GammaCorrectionShader.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. Gamma correction
composer.addPass(new ShaderPass(GammaCorrectionShader));

// 5. Anti-aliasing (always last before output)
const fxaaPass = new ShaderPass(FXAAShader);
fxaaPass.uniforms["resolution"].value.set(
1 / window.innerWidth,
1 / window.innerHeight,
);
composer.addPass(fxaaPass);


## Render to Texture

// 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

// Multiple composers for different scenes/layers
const bgComposer = new EffectComposer(renderer);
bgComposer.addPass(new RenderPass(bgScene, camera));

const fgComposer = new EffectComposer(renderer);
fgComposer.addPass(new RenderPass(fgScene, camera));
fgComposer.addPass(bloomPass);

// Combine in render loop
function animate() {
// Render background without clearing
renderer.autoClear = false;
renderer.clear();

bgComposer.render();

// Render foreground over it
renderer.clearDepth();
fgComposer.render();
}


## WebGPU Post-Processing (Three.js r150+)

import { postProcessing } from "three/addons/nodes/Nodes.js";
import { pass, bloom, dof } from "three/addons/nodes/Nodes.js";

// Using node-based system
const scenePass = pass(scene, camera);
const bloomNode = bloom(scenePass, 0.5, 0.4, 0.85);

const postProcessing = new THREE.PostProcessing(renderer);
postProcessing.outputNode = bloomNode;

// Render
function animate() {
postProcessing.render();
}


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

// Disable pass
bloomPass.enabled = false;

// Reduce bloom resolution
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth / 2, window.innerHeight / 2),
strength,
radius,
threshold,
);

// Only apply effects in high-performance scenarios
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
if (!isMobile) {
composer.addPass(expensivePass);
}


## Handle Resize

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),
);
}

if (bloomPass) {
bloomPass.resolution.set(width, height);
}
}

window.addEventListener("resize", onWindowResize);


## See Also

- threejs-shaders - Custom shader development
- threejs-textures - Render targets
- threejs-fundamentals - Renderer setup

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/threejs-postprocessing/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/cloudai-x/threejs-skills/threejs-postprocessing/SKILL.md
  • Cursor: ~/.cursor/skills/cloudai-x/threejs-skills/threejs-postprocessing/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/cloudai-x/threejs-skills/threejs-postprocessing/SKILL.md

🚀 Install with CLI:
npx skills add cloudai-x/threejs-skills

Read the Master Guide: Mastering Agent Skills

Related Skill Units

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid creative & visual issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Creative & Visual and is published by CloudAI-X, maintained in cloudai-x/threejs-skills.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.