Skip to content

Postprocessing Library Reference

A Three.js guide for coding agents. Also covers pmndrs postprocessing, postprocessing npm package, react-three postprocessing api, effectpass merging, effect catalog, selective bloom, and 28 more.

Show all 34 aliases

pmndrs postprocessing, postprocessing npm package, react-three postprocessing api, effectpass merging, effect catalog, selective bloom, mipmap blur bloom, depth of field effect, bokeh, autofocus, ssao, n8ao, smaa, fxaa, chromatic aberration, noise effect, vignette, god rays, outline effect, glitch effect, pixelation, dot screen, scanline, tone mapping effect, lut color grading, hue saturation, brightness contrast, tilt shift, blend function list, selection select components, effectattribute convolution, custom effect class, multisampling prop, enableNormalPass

The official API layer for pmndrs postprocessing and @react-three/postprocessing. Course-level composer mechanics, the DrunkEffect walkthrough, and basic R3F usage live in post-processing.md; this doc is the effect catalog and library contract.

Architecture: why this library over three's examples composer

  • Three's examples/jsm/postprocessing runs one full-screen render operation per pass. Ten passes means ten framebuffer round trips, each potentially doing its own depth or normal render
  • pmndrs postprocessing splits the concept in two: a Pass owns a render operation, an Effect owns only a fragment of shader logic. The EffectPass automatically organizes and merges any combination of effects into as few shader programs as possible, preserving your order, so many effects cost close to one pass
  • Every effect declares its own blend function, so "how does this layer combine" is part of the effect, not a separate composite step
  • Fullscreen work uses a single triangle instead of a quad, avoiding redundant fragment work along the diagonal
  • The library ships its own EffectComposer, RenderPass, EffectPass, NormalPass, DepthPass, and more. Same names as three's examples classes, different implementations; do not mix the two families

Vanilla setup

import { BloomEffect, EffectComposer, EffectPass, RenderPass } from "postprocessing"
import { HalfFloatType } from "three"

const composer = new EffectComposer(renderer, { frameBufferType: HalfFloatType })
composer.addPass(new RenderPass(scene, camera))
composer.addPass(new EffectPass(camera, new BloomEffect()))

requestAnimationFrame(function render() {
    requestAnimationFrame(render)
    composer.render()
})
  • One EffectPass takes many effects as constructor arguments; that is the merge. Only reach for multiple EffectPasses when an effect combination cannot merge (see convolution below)
  • frameBufferType: HalfFloatType is the recommended setup: default UnsignedByteType buffers band visibly in dark scenes, and HDR-ish values above 1 survive between passes for bloom and tone mapping
  • Set renderer.outputColorSpace = SRGBColorSpace and the library follows suit with sRGB framebuffers at the end of the chain
  • Disable renderer tone mapping (NoToneMapping) and add a ToneMappingEffect at the end of the pipeline instead, so tone mapping applies uniformly to the fully composed image

Effect catalog

One-liners for every effect worth knowing. All accept a blendFunction option; listed defaults come from the official docs.

  • BloomEffect: luminance-thresholded glow. With mipmapBlur: true it composites downscaled mip levels into wide, natural glows cheaply
  • SelectiveBloomEffect(scene, camera, options): bloom restricted to a Selection of objects, built on a depth mask
  • DepthOfFieldEffect: realistic bokeh blur driven by focusDistance, focalLength, bokehScale; the two distances are normalized against camera near/far, not world units
  • SSAOEffect: screen-space ambient occlusion, needs a normal buffer. Heavy; the community alternative is N8AO (separate n8ao package, re-exported by the R3F wrapper) which is faster and needs no normal pass
  • SMAAEffect: subpixel morphological antialiasing. Defaults: SMAAPreset.MEDIUM, EdgeDetectionMode.COLOR, blend SRC. Presets LOW to ULTRA trade search steps for quality
  • FXAAEffect: fast approximate antialiasing, cheaper and blurrier than SMAA. Pick FXAA for the lowest cost, SMAA for quality, or MSAA via the composer's multisampling instead of either
  • ChromaticAberrationEffect: RGB fringing via per-channel offset
  • NoiseEffect: film grain; ugly raw, usable with premultiply plus a soft blend function
  • VignetteEffect: darkened corners via offset and darkness
  • GodRaysEffect(camera, lightSource, options): volumetric light shafts radiating from a mesh (a sun sphere, a lamp)
  • OutlineEffect(scene, camera, options): edge outlines on a Selection. Defaults: blend SCREEN, edgeStrength 1, visibleEdgeColor 0xffffff, xray true (occluded parts still outlined), optional blur and pulseSpeed
  • GlitchEffect: periodic digital glitches, [min, max] ranges for delay/duration/strength, modes from GlitchMode. Flash warning
  • PixelationEffect(granularity = 30): mosaic pixelation
  • DotScreenEffect: halftone dot raster
  • ScanlineEffect: CRT scanlines
  • GridEffect: grid overlay pattern
  • ToneMappingEffect: HDR to display mapping. Default mode is ToneMappingMode.AGX; blend SRC. Reinhard2 variants use the luminance options (whitePoint, middleGrey, minLuminance, adaptationRate)
  • LUT3DEffect: color grading via a 3D lookup texture (.cube / LUT images loaded with the library's LUT loaders)
  • HueSaturationEffect: hue rotation and saturation scale
  • BrightnessContrastEffect: linear brightness and contrast
  • SepiaEffect: sepia tint
  • ColorAverageEffect, ColorDepthEffect: grayscale-average and bit-depth reduction
  • TiltShiftEffect: miniature look, a blurred band above and below a sharp focus line
  • ShockWaveEffect(camera, position, options): expanding distortion ring from a world position
  • TextureEffect: composite any texture over the frame; the base for overlays and watermarks

Recipes for the fiddly effects

SMAA and FXAA

import { SMAAEffect, SMAAPreset, EdgeDetectionMode, FXAAEffect } from "postprocessing"

const smaa = new SMAAEffect({
    preset: SMAAPreset.HIGH,              // LOW | MEDIUM (default) | HIGH | ULTRA
    edgeDetectionMode: EdgeDetectionMode.COLOR   // DEPTH | LUMA | COLOR
})

const fxaa = new FXAAEffect()   // knobs: minEdgeThreshold, maxEdgeThreshold, subpixelQuality, samples
  • SMAA carries EffectAttribute.CONVOLUTION | DEPTH, so it cannot share an EffectPass with another convolution effect
  • Decision rule: MSAA (multisampling / samples on the target) for geometric edges when the budget allows, SMAA for the best post-based quality, FXAA when every millisecond counts

God rays

import { GodRaysEffect } from "postprocessing"

const sun = new Mesh(new SphereGeometry(1, 32, 32),
    new MeshBasicMaterial({ color: 0xffddaa }))
scene.add(sun)

const godRays = new GodRaysEffect(camera, sun, { density: 0.96, decay: 0.92 })
  • The second argument is a real mesh in the scene, not a light; the effect renders it into an occlusion buffer and streaks from its screen position

LUT color grading

import { LUT3DEffect, LookupTexture } from "postprocessing"

// From a .cube / LUT image via the library loaders, or generate neutral and edit:
const lut = LookupTexture.createNeutral(32)
const lutEffect = new LUT3DEffect(lut)
  • Grade once in a desktop tool, export the LUT, ship one texture instead of stacking HueSaturation + BrightnessContrast at runtime

Kernel sizes

  • Blur-based effects (Bloom, Outline blur, TiltShift, DoF internals) take a kernelSize from the KernelSize enum: VERY_SMALL, SMALL, MEDIUM, LARGE, VERY_LARGE, HUGE. Bigger kernel, wider and costlier blur; mipmapBlur bloom sidesteps the tradeoff

Blend functions

  • Every effect owns a BlendMode created from its blendFunction option; change it at runtime via effect.blendMode.blendFunction and scale it with blendMode.opacity
  • The full BlendFunction enum: ADD, ALPHA, AVERAGE, COLOR, COLOR_BURN, COLOR_DODGE, DARKEN, DIFFERENCE, DIVIDE, DST, EXCLUSION, HARD_LIGHT, HARD_MIX, HUE, INVERT, INVERT_RGB, LIGHTEN, LINEAR_BURN, LINEAR_DODGE, LINEAR_LIGHT, LUMINOSITY, MULTIPLY, NEGATION, NORMAL, OVERLAY, PIN_LIGHT, REFLECT, SATURATION, SCREEN, SOFT_LIGHT, SRC, SUBTRACT, VIVID_LIGHT
  • NORMAL is the general default; SRC (output replaces input entirely) is the default for full-image effects like SMAA, FXAA, and ToneMapping
  • SKIP and SET are deprecated. Do not use SKIP to disable an effect; remove the effect or disable its pass instead
  • Cycling blend functions in a debug UI beats guessing; the same effect reads completely differently under SOFT_LIGHT vs OVERLAY vs SCREEN

Selection and selective effects

  • Selection (from postprocessing) is a set of objects backed by a three.js layer. Selective effects (SelectiveBloomEffect, OutlineEffect) expose a selection property; selection.add(mesh) / selection.delete(mesh) control membership, selectionLayer picks which layer is used
  • In R3F the declarative equivalent is the <Selection> context plus <Select enabled> groups:
<Selection>
  <EffectComposer autoClear={false}>
    <Outline blur edgeStrength={100} />
  </EffectComposer>
  <Select enabled>
    <mesh />
  </Select>
</Selection>
  • <Select enabled> recursively tags child meshes into the selection context; toggle enabled (for example on hover) to move objects in and out
  • autoClear={false} on the composer is part of the documented Outline selection recipe
  • Effect components also accept a selection prop of refs directly (selection={[meshRef1, meshRef2]}) when you do not want the context
  • R3F <SelectiveBloom> additionally REQUIRES a lights array of refs to all relevant lights, or lit objects misbehave

Custom Effect class contract

post-processing.md covers the full walkthrough (mainImage, mainUv, uniforms Map, update, React wrapper). The remaining API surface:

super("MyEffect", fragmentShader, {
    attributes: EffectAttribute.NONE,   // or CONVOLUTION, DEPTH, bitwise OR-able
    blendFunction: BlendFunction.NORMAL,
    defines: new Map([["SAMPLES", "16"]]),
    uniforms: new Map([["intensity", new Uniform(1)]]),
    extensions: null,                   // Set of WebGLExtension if required
    vertexShader: null                  // rarely needed; adds VERTEX_MAIN_SUPPORT code
})
  • attributes declare execution priority and resource needs. EffectAttribute.DEPTH requests the depth texture (a depth sampler becomes available). EffectAttribute.CONVOLUTION marks an effect that samples neighboring pixels (blurs, bloom, SMAA)
  • Convolution constraints imposed by the merged-shader design: an EffectPass can contain at most ONE convolution effect, and convolution effects cannot merge with effects that transform UVs (mainUv) or read arbitrary offsets. Pixelation, for example, is documented as unable to merge with convolution effects. When the combination is illegal the library throws at pass creation; split the offenders into separate EffectPasses
  • Ordering inside a merged pass follows the order you pass effects in, except attributes raise priority: depth-based and convolution effects are scheduled by the merger. Between passes, order is exactly addPass order
  • defines are preprocessor macros; after changing defines, uniforms, or extensions at runtime call effect.setChanged() so the EffectPass rebuilds its merged shader. Changing only a uniform VALUE needs no rebuild
  • Shader code is spliced into placeholders (EffectShaderSection.FRAGMENT_HEAD, FRAGMENT_MAIN_UV, FRAGMENT_MAIN_IMAGE), which is why the mainImage / mainUv signatures must match exactly
  • Effects are meant to be single-purpose; implement update(renderer, inputBuffer, deltaTime) for per-frame work and dispose() if you allocate targets

R3F wrapper specifics (@react-three/postprocessing)

  • Every effect component maps its props 1:1 to the underlying effect's constructor options: <Bloom luminanceThreshold={1.1} mipmapBlur /> is new BloomEffect({ luminanceThreshold: 1.1, mipmapBlur: true }). Anything in the vanilla docs is reachable as a prop
  • Import enums (BlendFunction, ToneMappingMode, KernelSize, SMAAPreset, GlitchMode) from postprocessing itself; keep it as an explicit dependency
  • <EffectComposer> props (from the official API reference):
    • multisampling: MSAA sample count, default 8; set 0 to disable for performance
    • enableNormalPass: allocates the normal buffer that SSAO and other normal-aware effects need; leave it off otherwise, the extra scene render is not free
    • depthBuffer, stencilBuffer: buffer allocation toggles
    • resolutionScale: downscale factor for resolution-aware passes
    • frameBufferType: default HalfFloatType
    • autoClear (default true), renderPriority (default 1), camera / scene overrides, enabled, and a ref to the underlying composer instance
  • The composer rebuilds its passes when children change; drive uniforms imperatively (refs + useFrame) instead of re-rendering prop changes every frame
  • <Autofocus>: extends DepthOfField and animates focus. Props: target ([x, y, z]), mouse (follow the pointer), manual, smoothTime (default 0.25), debug. Drop it in and focus tracks the screen center by default
  • <N8AO>: wrapper around the third-party n8ao package, the practical SSAO replacement: better quality per millisecond and no enableNormalPass requirement
  • Keep <ToneMapping> as the last child; default mode AGX, most R3F scenes are tuned for ToneMappingMode.ACES_FILMIC (details in post-processing.md)

A composer tuned for a low-end device, from the official patterns doc:

<EffectComposer
  resolutionScale={0.5}   // half resolution
  multisampling={4}       // reduced MSAA
  enableNormalPass        // only because SSAO below needs it
>
  <SSAO samples={16} radius={15} />
  <Bloom height={300} />   // smaller internal bloom texture
  <Noise opacity={0.01} />
</EffectComposer>

Driving effect uniforms per frame in R3F

  • Hold a ref to the effect component and mutate the instance in useFrame; do not set React props per frame, every prop change risks reconstructing the effect and rebuilding the merged shader
const bloomRef = useRef()
useFrame((state) => {
  bloomRef.current.intensity = 1 + Math.sin(state.clock.elapsedTime)
})
return (
  <EffectComposer>
    <Bloom ref={bloomRef} mipmapBlur luminanceThreshold={1} />
  </EffectComposer>
)

Performance guidance

  • Prefer effects merged into one EffectPass over many passes; the whole point of the library is that a merged pass is close to the cost of one
  • Cut the biggest costs first: multisampling={0} when SMAA or the look allows it, no enableNormalPass unless an effect needs normals, resolutionScale below 1 on fill-rate-bound scenes
  • Half-resolution knobs exist per effect too: Bloom and blur effects take width / height (or resolutionScale) for their internal targets; the docs' own perf recipe uses <Bloom height={300} /> and reduced <SSAO samples>
  • mipmapBlur bloom is the cheap wide glow: the blur happens on progressively smaller mip targets, so large radii do not multiply cost the way big blur kernels do
  • Convolution effects (blur, bloom, SMAA, DoF) are the expensive family; each forces its own sampling loops and may force an extra pass split. Budget them, not the color-grading effects, which merge into nearly free arithmetic
  • HalfFloatType costs bandwidth vs UnsignedByteType but prevents banding; keep it unless profiling a low-end target says otherwise

Common pitfalls (from the official docs)

  • Mixing classes from three/examples/jsm/postprocessing with postprocessing ones; same names, incompatible pipelines
  • Leaving renderer.toneMapping on while also adding a ToneMappingEffect, double-mapping the image; disable the renderer's and tone map at the end of the chain
  • Skipping the HalfFloat framebuffer and getting banding in dark gradients, or bloom starved because values clip at 1 between passes
  • Putting two convolution effects in one EffectPass, or a convolution effect together with a UV-transforming effect, and hitting a shader merge error; split into separate passes
  • Using BlendFunction.SKIP to "disable" an effect; it is deprecated and the wiki-documented path is disabling the pass or removing the effect
  • Forgetting effect.setChanged() after mutating defines or adding/removing uniforms, so the merged shader never recompiles
  • Transparent objects do not write depth the way depth-based effects expect: DepthOfField, SSAO, and depth-masked SelectiveBloom read the opaque depth buffer, so transparent surfaces blur or occlude incorrectly. Keep critical objects opaque or exclude them
  • In R3F, enabling enableNormalPass "just in case": it renders the scene again for normals every frame even if nothing reads them
  • Forgetting lights on <SelectiveBloom> (documented as required) or the selectionLayer collision when two selective effects share a layer
  • Reading DepthOfField focusDistance / focalLength as world units; they are normalized against the camera near/far range

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-three-js-guide({ topic: "postprocessing-library-reference" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems