Three.js From Zero · Article s13-08
R3F Post-Processing
R3F Post-Processing is Article s13-08 of Three.js From Zero, a MasterAllArts free interactive lesson for artists learning creative 3D on the web.
Season 13 · Article 08 · R3F Mastery
The cinematic layer. @react-three/postprocessing wraps the postprocessing library with JSX components: bloom, depth of field, vignette, chromatic aberration, custom shader passes. The "make it look rendered" toolkit.
Code-walkthrough article
Install: npm i @react-three/postprocessing postprocessing. Same effect chain Bruno uses — bloom is what people are really there for.
The effect composer pattern
import { EffectComposer, Bloom, Vignette, DepthOfField, ChromaticAberration } from '@react-three/postprocessing';
<Canvas>
{/* scene */}
<EffectComposer>
<Bloom intensity={1.5} luminanceThreshold={0.9} />
<DepthOfField focusDistance={0.02} focalLength={0.05} bokehScale={4} />
<Vignette eskil={false} offset={0.3} darkness={0.6} />
<ChromaticAberration offset={[0.001, 0.001]} />
</EffectComposer>
</Canvas>
Order matters: bloom before DOF (you want the blur applied to the bloomed image, not vice versa). Vignette and chromatic aberration go last — they're "screen" effects.
Bloom — your most-used effect
<Bloom
intensity={1.5} // how bright the glow
luminanceThreshold={0.9} // pixels above this brightness bloom
luminanceSmoothing={0.025} // soft edge to the threshold
mipmapBlur // faster on mobile, slightly less accurate
/>
Bloom is the "neon city" effect. luminanceThreshold controls what blooms — set to 0.9, only the brightest emissive surfaces glow. Set to 0.3, half the scene glows. Most beginners crank intensity instead of threshold; that washes out the image. Tune threshold first.
Making emissive materials
<meshStandardMaterial
color="#ec4899"
emissive="#ff5fa0"
emissiveIntensity={3} // > 1 to push above bloom threshold
toneMapped={false} // critical: don't tone-map emissives below 1
/>
toneMapped={false} is the key — the renderer's tone mapping compresses HDR values to LDR before output. Without disabling it on your emissive material, your emissive can never push past the bloom threshold no matter how high you crank intensity.
Depth of field
<DepthOfField
focusDistance={0.02} // 0-1, distance from camera in normalized depth
focalLength={0.05} // how deep the in-focus band is
bokehScale={4} // strength of out-of-focus blur
/>
The product-photography effect. Subject in razor focus, foreground/background dreamy. Combine with autofocus: focusDistance driven by raycaster — the object the user's cursor is on is always in focus.
Custom shader pass
import { Effect } from 'postprocessing';
class GrainEffect extends Effect {
constructor() {
super('GrainEffect', /*glsl*/`
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
float grain = fract(sin(dot(uv * time, vec2(12.9898, 78.233))) * 43758.5453);
outputColor = inputColor + vec4(grain * 0.05);
}
`, { attributes: [{ name: 'time', type: 'f', defaultValue: 0 }] });
}
}
Your own pass when you need something non-standard. Film grain, scanlines, custom tone-mapping — all single shader passes.
Common first-time pitfalls
Exercises
- Tune your bloom. Take a scene with one emissive object. Sweep luminanceThreshold from 0.1 to 1.0 in your scene. Identify the value where ONLY your emissive glows. That's your project's setting.
- Cinematic still. Add DepthOfField + Vignette + ChromaticAberration. Compare with and without. Screenshot both.
- Pulse the bloom. Animate intensity over time with useFrame:
bloom.intensity = 1 + Math.sin(t) * 0.5. Breathing neon.
UP NEXT
S13-09 — R3F Custom Shaders → shaderMaterial helper, TypeScript types for shader props.