Course outline
The Effect Chain
In one line
Post-processing draws the scene into a picture, then changes the picture with a chain of passes. On WebGL that is EffectComposer, and the chain must end with OutputPass once. On WebGPU a node pipeline does the same.
Think of it like this
A photo studio. The photographer takes the raw photo (the render pass), the editor brightens a window (bloom), draws an outline round the product (outline pass), and last the printer converts everything to the paper's colours (output). If the printer's conversion is skipped the print is dark; if it is done twice the print is pale.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- post-processing
- Changing the picture after the scene is drawn, with full-screen shader passes.
- pass
- One step of the chain: it reads the previous picture and writes a new one.
- tone mapping
- Squeezing very bright values into the range a screen can show, with a pleasant curve.
- sRGB curve
- The brightness curve screens expect. Linear values are encoded with it just before the screen.
- node pipeline
- WebGPU's way to build effects: small connected nodes (TSL) instead of a list of passes.
Step by step
01Draw the scene without any effect
The first example paints an orange half and a grey half with flat colours. renderer.render gives pixels 255, 105, 45 and 130, 130, 130 (rounded to steps of 5, because GPUs differ by a step or two).
02Leave out OutputPass
Render + FXAA with no OutputPass gives 255, 35, 5 and 55, 55, 55. The renderer's RenderPass wrote linear values into the target and FXAA copied them to the canvas without the sRGB curve: the colours are dark and over-saturated.
03Add OutputPass once
Render + Output + FXAA matches the direct render exactly: 255, 105, 45. OutputPass did the tone mapping (none here) and the sRGB curve, and FXAA works on finished colours.
04Add it twice
Two OutputPasses apply the curve twice: grey 130 becomes 190 and the orange turns pale, 255, 175, 115. This is the washed-out look. It often appears when a helper already adds an output step.
Try it yourself
- 1
Switch the tone mapping on
In the first example add
renderer.toneMapping = THREE.ACESFilmicToneMapping;aftersetSize. The direct render and the chain with oneOutputPassstill agree, now at245, 130, 55for the orange:OutputPassreads the renderer's tone mapping. WithoutOutputPassit stays255, 35, 5: nothing applied it. - 2
Let the status bar count the whole frame
In the second example delete
renderer.info.autoReset = false;. TheRender + OutputandRender + Bloom + Outputlines now saydraw calls: 1,triangles: 1: only the last full-screen draw is counted.
Code & diagrams
Pixel values are rounded to steps of 5. The last line is the washed-out look.
Console
Run the scene to see its console.Expected console output
renderer.render (direct) orange: 255, 105, 45 grey: 130, 130, 130
Render + FXAA (no OutputPass) orange: 255, 35, 5 grey: 55, 55, 55
Render + Output + FXAA orange: 255, 105, 45 grey: 130, 130, 130
Render + Output + Output + FXAA orange: 255, 175, 115 grey: 190, 190, 190info.autoReset is off so the numbers cover the whole frame, not only the last pass.
Console
Run the scene to see its console.Expected console output
renderer.render draw calls: 3 | triangles: 1584 | textures on the GPU: 1
Render pass only draw calls: 3 | triangles: 1584 | textures on the GPU: 1
Render + Output draw calls: 4 | triangles: 1585 | textures on the GPU: 2
Render + Bloom + Output draw calls: 17 | triangles: 1598 | textures on the GPU: 14
passes: RenderPass > UnrealBloomPass > OutputPassThe output depends on the browser. With WebGPU it logs `backend: WebGPU`. Without, it warns `THREE.WebGPURenderer: WebGPU is not available, running under WebGL2 backend.` and logs `backend: WebGL 2 (fallback)`. Both then log `... applied by the pipeline itself: true | scene pass samples: 4`.
Console
Run the scene to see its console.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Two OutputPasses
In the first example the last chain already has two. Compare it with Render + Output + FXAA.
Break #2
Chain without OutputPass
In the first example read the line Render + FXAA (no OutputPass).
Myth vs fact
Myth
Post-processing is a free filter on top of the scene.
Fact
Each pass draws the whole screen again and keeps its own pictures in memory: bloom added 13 draw calls and 12 textures above.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
UnrealBloomPassis a special case in r186: as the last pass it draws its input to the canvas through aMeshBasicMaterial, so tone mapping and encoding apply. A chain of onlyRenderPassand bloom can look right withoutOutputPass, and breaks when you add a pass after it. Rely on the rule. - ▸
The
postprocessinglibrary (6.39.5) is the other well-known choice.ItsEffectComposermerges many effects into one shader pass, so five effects cost fewer full-screen draws than five three.js passes, andmultisamplinggives MSAA:new EffectComposer(renderer, { multisampling: 4 }),addPass(new RenderPass(...)),addPass(new EffectPass(camera, new BloomEffect())).@react-three/postprocessingwraps it for React Three Fiber (Phase 11 teaches R3F itself). - ▸
The background is part of the picture.
renderer.renderclears to the background colour without tone mapping. Inside the chain the background is drawn into the linear target andOutputPasstone-maps it with everything else. A scene withscene.background = new THREE.Color("#0b1118")andNeutralToneMappingreads11, 17, 24drawn directly and0, 8, 18through the chain (measured in a scratch scene;ACESFilmicToneMappinggives1, 3, 6). With no tone mapping both agree. When you add a chain, pick the backdrop colour with the chain on. The build's showroom shows it. - ▸
Resize and pixel ratio.
EffectComposerreadsrenderer.getPixelRatio()once, when it is created, andcomposer.setSize(w, h)multiplies by that remembered ratio. If the ratio changes later (a window dragged to another screen), callcomposer.setPixelRatio(renderer.getPixelRatio())too, or the chain stays soft.
Remember this
- 1
The problem this solves: the canvas shows one finished picture, but many looks (glow, outlines, smoothing edges) are made by processing the whole picture after it is drawn. You need a chain that passes a picture from step to step, and a rule for when colours are converted for the screen.
- 2
The WebGL chain.
new EffectComposer(renderer)owns two render targets (half-float by default) and a list of passes.composer.addPass(...)adds them in order.composer.render()replacesrenderer.render(): each pass reads one target and writes the other, and the last pass writes to the canvas. Typical order:RenderPass(draw the scene), your effects,OutputPass. - 3
Colours stay linear until the end. Inside the chain the values are linear and can go above 1 (a bright lamp).
OutputPassapplies the renderer'stoneMappingand the sRGB curve, once. Passes that work on the finished picture, like FXAA or SMAA, belong after it. - 4
What a chain costs. Every pass is at least one full-screen draw. The second example (
renderer.info.autoReset = false) shows a frame of three spheres: 3 draw calls plain, 4 withOutputPass, 17 with bloom added, and the textures on the GPU go from 1 to 14. With the defaultautoReset = truethe status bar shows only the last pass:draw calls: 1. - 5
What if the GPU cannot do half-float? The chain draws into half-float pictures, which WebGL 2 supports through
EXT_color_buffer_floatorEXT_color_buffer_half_float. If neither exists, keeprenderer.render(): no glow, but the page works. The build checks this. - 6
The WebGPU way. There is no composer.
pass(scene, camera)makes a node for the scene picture,bloom(...)and other nodes read it, and aRenderPipelinedraws the final node. Tone mapping and colour space are added by the pipeline itself (outputColorTransformistrue), so there is noOutputPass. The third example runs it; on a browser without WebGPU it runs on WebGL 2 and warns.
Explain it without notes
Why must OutputPass come before FXAA, and why only once?
A teammate adds bloom to a page and the colours look washed out. List three things to check.
Practice
List the passes, in order, for a chain with an outline, bloom and FXAA.
Write a function createChain(renderer, scene, camera) that returns a plain renderer call when the GPU cannot draw into half-float targets.
Done when you can
Build an
EffectComposerchain: RenderPass, effects, OutputPassExplain why colours go dark or pale when OutputPass is missing or doubled
Count what a chain adds to a frame with
renderer.infoName the WebGPU equivalent of a composer and of OutputPass