Navigate

Search topics across all sections

GitHub
Post-Processing

Effect Composer

You have rendered a beautiful 3D scene. Now you want to make it look cinematic -- glowing lights, soft vignettes, maybe a dreamy color grade. EffectComposer is the filter pipeline that makes it happen.

You add Bloom, Vignette, SSAO, Depth of Field, and Chromatic Aberration to your scene all at once. It looks amazing on your laptop. You send the link to your friend on a phone, and they report 8 frames per second and a very hot device.

terminal
WebGL: CONTEXT_LOST_WEBGL: loseContext — GPU ran out of resources on mobile device.

Real-world

Think of EffectComposer like the Instagram filter pipeline.

You snap a photo (your rendered 3D scene). Then you stack filters on top: a glow filter (Bloom), a dark-edges filter (Vignette), a color grading filter. Each filter processes the photo and passes the result to the next one.

The clever part? Instagram merges multiple simple filters into a single processing step so your phone does not melt. EffectComposer does the exact same thing -- it merges compatible effects into one shader pass. That is why it is much faster than applying each effect separately.

Render Scene

Take the photo

EffectComposer

The filter pipeline

Merge Effects

Combine compatible filters

Final Image

What you see!

Step by Step: Your First Filter Pipeline

Setting up EffectComposer is like opening Instagram and choosing your filters. Three quick steps and your scene goes from plain to cinematic.

Step 1: Install the filter pipeline

Scene.tsxTSX
import { EffectComposer, Bloom, Vignette }
  from '@react-three/postprocessing'

The @react-three/postprocessing library wraps the postprocessing library in React components. Import EffectComposer (the pipeline) and the effects you want.

Step 2: Add it to your Canvas

App.tsxTSX
<Canvas>
  <Scene />
  <EffectComposer>
    <Bloom intensity={1.5} mipmapBlur />
    <Vignette offset={0.3} darkness={0.9} />
  </EffectComposer>
</Canvas>

Drop EffectComposer inside Canvas and nest effects as children. That is it. The pipeline processes effects in child order, merging compatible ones automatically.

Step 3: Disable Canvas anti-aliasing

App.tsxTSX
<Canvas gl={{ antialias: false }}>
  <Scene />
  <EffectComposer multisampling={8}>
    <Bloom intensity={1.5} mipmapBlur />
  </EffectComposer>
</Canvas>

The Canvas antialias prop does not work with post-processing. Instead, use the composer's multisampling prop or add an SMAA effect for anti-aliasing.

Selective Bloom

Want only certain objects to glow? Wrap your scene in Selection and tag glowing objects with Select.

SelectiveBloom.tsxTSX
<Selection>
  <EffectComposer>
    <Bloom intensity={2} mipmapBlur />
  </EffectComposer>
  <Select enabled>
    <mesh> {/* This glows */} </mesh>
  </Select>
  <mesh> {/* This does NOT glow */} </mesh>
</Selection>

What you just learned

EffectComposer is the filter pipeline that processes your rendered scene with screen-space effects.

Compatible effects get merged into a single shader pass automatically, saving GPU work.

Always disable Canvas antialias and use the composer's multisampling or SMAA instead.

Control effects via intensity (0 = off) rather than conditional rendering to avoid shader recompilation.

Selective bloom lets you make only certain objects glow using Selection and Select wrappers.

Question

If EffectComposer merges compatible effects into one pass, why not add all effects and let it figure things out? What stops the merging from keeping everything fast?

Think about it...

You have a scene running at 60fps. You add Bloom, Vignette, and Noise inside EffectComposer. How many extra render passes will this cost?

Hint: Think about which effects need to analyze the whole image (like blurring bright areas) vs. which just tweak each pixel independently.

Try These Challenges

Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.

Try This!

Beginner

Set emissiveIntensity to 5 — nuclear glow

Try This!

Beginner

Set rotationSpeed to 0 — photo mode

Try This!

Beginner

Change torus color — new mood

These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.

1
Using Canvas antialias with post-processing
Built-in MSAA has no effect when effects are active
Don't do this
AntiAliasing.tsxTSX
// Canvas AA doesn't reach the post-processing buffer
<Canvas gl={{ antialias: true }}>
  <EffectComposer>
    <Bloom intensity={1} />
  </EffectComposer>
</Canvas>
Post-processing renders your scene to an offscreen buffer, not the screen. The Canvas antialias setting only applies to the default framebuffer, so it does nothing. Use the EffectComposer's multisampling prop or add an SMAA effect instead.
2
Adding every effect at once
GPU costs stack up fast, especially on mobile
Don't do this
MeasuredEffects.tsxTSX
// The kitchen sink — every effect turned on
<EffectComposer>
  <SSAO />
  <Bloom />
  <DepthOfField />
  <ChromaticAberration />
  <GodRays />
  <Noise />
  <Vignette />
</EffectComposer>
Each effect processes every pixel on screen. Heavy effects like SSAO and DoF each need their own render pass. On a phone, even two heavy effects can cut your frame rate in half. Start with Bloom + Vignette, measure with r3f-perf, then add more if you have room.
3
Toggling effects with conditional rendering
Mounting and unmounting recompiles the merged shader
Don't do this
ToggleEffects.tsxTSX
// Adding/removing the effect recompiles shaders
<EffectComposer>
  {bloom && <Bloom intensity={1.5} />}
  <Vignette />
</EffectComposer>
When you conditionally render an effect, EffectComposer must rebuild and recompile the merged shader. This causes a visible frame hitch. Keep the effect mounted and set its intensity to 0 instead -- that disables it without triggering recompilation.

Best Practices

Disable Canvas AA

Always set gl={{ antialias: false }} on Canvas when using post-processing. Use the composer's multisampling prop or add an SMAA effect for anti-aliasing instead.

Start with Two Effects

Bloom + Vignette is a great starting combo. Add effects one at a time, measuring GPU impact with r3f-perf after each addition. Mobile GPUs have much less headroom than desktop.

Use mipmapBlur for Bloom

The mipmapBlur prop on Bloom produces physically accurate, high-quality glow with minimal performance cost. Always prefer it over the default blur method.

Toggle via Intensity, Not Mounting

Never use conditional rendering to toggle effects. Set intensity to 0 to disable an effect without triggering costly shader recompilation. The effect stays in memory but costs nothing.