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/postprocessingruns one full-screen render operation per pass. Ten passes means ten framebuffer round trips, each potentially doing its own depth or normal render - pmndrs
postprocessingsplits the concept in two: aPassowns a render operation, anEffectowns only a fragment of shader logic. TheEffectPassautomatically 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
EffectPasstakes many effects as constructor arguments; that is the merge. Only reach for multiple EffectPasses when an effect combination cannot merge (see convolution below) frameBufferType: HalfFloatTypeis the recommended setup: defaultUnsignedByteTypebuffers band visibly in dark scenes, and HDR-ish values above 1 survive between passes for bloom and tone mapping- Set
renderer.outputColorSpace = SRGBColorSpaceand the library follows suit with sRGB framebuffers at the end of the chain - Disable renderer tone mapping (
NoToneMapping) and add aToneMappingEffectat 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. WithmipmapBlur: trueit composites downscaled mip levels into wide, natural glows cheaplySelectiveBloomEffect(scene, camera, options): bloom restricted to aSelectionof objects, built on a depth maskDepthOfFieldEffect: realistic bokeh blur driven byfocusDistance,focalLength,bokehScale; the two distances are normalized against camera near/far, not world unitsSSAOEffect: screen-space ambient occlusion, needs a normal buffer. Heavy; the community alternative is N8AO (separaten8aopackage, re-exported by the R3F wrapper) which is faster and needs no normal passSMAAEffect: subpixel morphological antialiasing. Defaults:SMAAPreset.MEDIUM,EdgeDetectionMode.COLOR, blendSRC. Presets LOW to ULTRA trade search steps for qualityFXAAEffect: fast approximate antialiasing, cheaper and blurrier than SMAA. Pick FXAA for the lowest cost, SMAA for quality, or MSAA via the composer'smultisamplinginstead of eitherChromaticAberrationEffect: RGB fringing via per-channel offsetNoiseEffect: film grain; ugly raw, usable withpremultiplyplus a soft blend functionVignetteEffect: darkened corners viaoffsetanddarknessGodRaysEffect(camera, lightSource, options): volumetric light shafts radiating from a mesh (a sun sphere, a lamp)OutlineEffect(scene, camera, options): edge outlines on aSelection. Defaults: blendSCREEN,edgeStrength1,visibleEdgeColor0xffffff,xraytrue (occluded parts still outlined), optionalblurandpulseSpeedGlitchEffect: periodic digital glitches,[min, max]ranges for delay/duration/strength, modes fromGlitchMode. Flash warningPixelationEffect(granularity = 30): mosaic pixelationDotScreenEffect: halftone dot rasterScanlineEffect: CRT scanlinesGridEffect: grid overlay patternToneMappingEffect: HDR to display mapping. Default mode isToneMappingMode.AGX; blendSRC. 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 scaleBrightnessContrastEffect: linear brightness and contrastSepiaEffect: sepia tintColorAverageEffect,ColorDepthEffect: grayscale-average and bit-depth reductionTiltShiftEffect: miniature look, a blurred band above and below a sharp focus lineShockWaveEffect(camera, position, options): expanding distortion ring from a world positionTextureEffect: 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/sampleson 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
kernelSizefrom theKernelSizeenum: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
BlendModecreated from itsblendFunctionoption; change it at runtime viaeffect.blendMode.blendFunctionand scale it withblendMode.opacity - The full
BlendFunctionenum: 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 NORMALis the general default;SRC(output replaces input entirely) is the default for full-image effects like SMAA, FXAA, and ToneMappingSKIPandSETare 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(frompostprocessing) is a set of objects backed by a three.js layer. Selective effects (SelectiveBloomEffect,OutlineEffect) expose aselectionproperty;selection.add(mesh)/selection.delete(mesh)control membership,selectionLayerpicks 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; toggleenabled(for example on hover) to move objects in and outautoClear={false}on the composer is part of the documented Outline selection recipe- Effect components also accept a
selectionprop of refs directly (selection={[meshRef1, meshRef2]}) when you do not want the context - R3F
<SelectiveBloom>additionally REQUIRES alightsarray 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
})
attributesdeclare execution priority and resource needs.EffectAttribute.DEPTHrequests the depth texture (adepthsampler becomes available).EffectAttribute.CONVOLUTIONmarks 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
addPassorder definesare preprocessor macros; after changingdefines,uniforms, orextensionsat runtime calleffect.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 themainImage/mainUvsignatures must match exactly - Effects are meant to be single-purpose; implement
update(renderer, inputBuffer, deltaTime)for per-frame work anddispose()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 />isnew BloomEffect({ luminanceThreshold: 1.1, mipmapBlur: true }). Anything in the vanilla docs is reachable as a prop - Import enums (
BlendFunction,ToneMappingMode,KernelSize,SMAAPreset,GlitchMode) frompostprocessingitself; keep it as an explicit dependency <EffectComposer>props (from the official API reference):multisampling: MSAA sample count, default 8; set 0 to disable for performanceenableNormalPass: allocates the normal buffer that SSAO and other normal-aware effects need; leave it off otherwise, the extra scene render is not freedepthBuffer,stencilBuffer: buffer allocation togglesresolutionScale: downscale factor for resolution-aware passesframeBufferType: defaultHalfFloatTypeautoClear(default true),renderPriority(default 1),camera/sceneoverrides,enabled, and arefto 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-partyn8aopackage, the practical SSAO replacement: better quality per millisecond and noenableNormalPassrequirement- Keep
<ToneMapping>as the last child; default mode AGX, most R3F scenes are tuned forToneMappingMode.ACES_FILMIC(details inpost-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, noenableNormalPassunless an effect needs normals,resolutionScalebelow 1 on fill-rate-bound scenes - Half-resolution knobs exist per effect too: Bloom and blur effects take
width/height(orresolutionScale) for their internal targets; the docs' own perf recipe uses<Bloom height={300} />and reduced<SSAO samples> mipmapBlurbloom 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
HalfFloatTypecosts bandwidth vsUnsignedByteTypebut prevents banding; keep it unless profiling a low-end target says otherwise
Common pitfalls (from the official docs)
- Mixing classes from
three/examples/jsm/postprocessingwithpostprocessingones; same names, incompatible pipelines - Leaving
renderer.toneMappingon while also adding aToneMappingEffect, 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.SKIPto "disable" an effect; it is deprecated and the wiki-documented path is disabling the pass or removing the effect - Forgetting
effect.setChanged()after mutatingdefinesor 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
lightson<SelectiveBloom>(documented as required) or theselectionLayercollision when two selective effects share a layer - Reading DepthOfField
focusDistance/focalLengthas world units; they are normalized against the camera near/far range