Course outline
Render Targets
In one line
A render target is a picture the GPU draws into instead of the screen. It becomes a texture you can show on a mirror, a screen inside the scene, or feed to an effect. It costs memory, and it does not resize by itself.
Think of it like this
A photographer takes a photo of the showroom, prints it, and pins the print on the wall as a poster. The poster is a picture of the room, inside the room. A render target is that print: the GPU first draws a scene into a hidden picture, then another scene uses the picture as a texture.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- render target
- A picture the GPU draws into instead of the screen. In three.js, a
WebGLRenderTarget. - framebuffer
- The GPU object that says where drawing goes: the canvas, or a texture and its depth buffer. A render target wraps one.
- half-float
- A 16-bit decimal number per colour channel. It can store values above 1, which a normal 8-bit target cannot.
- feedback loop
- Reading from a texture while drawing into it. The GPU does not allow it.
- ping-pong
- Two targets that swap roles each pass: read A and write B, then read B and write A.
Step by step
01Make a small world to photograph
The example builds a second scene (photoScene) with a normal-material cube and its own camera. This scene never goes to the canvas. It exists to fill the target.
02Draw into the target, then into the canvas
Each frame does two render() calls. First setRenderTarget(target) and render the photo scene. Then setRenderTarget(null) and render the real scene, whose plane uses target.texture as its map.
The status bar reads draw calls: 1: it shows only the last call (Topic 0.3). The cube's call into the target is not counted.
03Read the numbers
The console says the target is 256 × 256, its texture is a render-target texture, and the picture is 256 × 256 × 4 = 262,144 bytes. The GPU holds 1 texture: the target's. The depth buffer is a separate renderbuffer, not counted.
04Match the canvas, in real pixels
The second example creates a 300 × 150 target, grows the canvas to 600 × 300, and the target stays put. At pixel ratio 2 the canvas has 1200 × 600 real pixels. getDrawingBufferSize gives the number to copy into target.setSize.
Try it yourself
- 1
Shrink the picture
In the first example change
256, 256to64, 64. The console printstarget: 64 x 64andsize of the picture: 16384 bytes, and the cube on the screen turns blocky and soft: 64 pixels are stretched over the plane. - 2
Match the pixel ratio
In the second example change
setPixelRatio(2)tosetPixelRatio(3). Thefixed:line now readscanvas 1800 x 900 | target 1800 x 900: the same 600 × 300 box holds nine times the pixels of ratio 1.
Code & diagrams
The cube is drawn into a 256 × 256 texture, and a plane in another scene shows that texture. Watch the status bar: the geometry count and texture count.
Console
Run the scene to see its console.Expected console output
target: 256 x 256 | is a render-target texture: true
textures on the GPU: 1 | draw calls in the last render: 1
size of the picture: 262144 bytes (4 per pixel)Sizes are printed in real pixels. The last two lines are plain arithmetic: width × height × bytes per pixel.
Console
Run the scene to see its console.Expected console output
start: canvas 300 x 150 | target 300 x 150
resized: canvas 600 x 300 | target 300 x 150 <- the target did not follow
fixed: canvas 1200 x 600 | target 1200 x 600
1920 x 1080, ratio 1 | 8-bit RGBA 7.9 MB | half-float RGBA 15.8 MB | a composer holds two of the half-float ones: 31.6 MB
390 x 844, ratio 3 | 8-bit RGBA 11.3 MB | half-float RGBA 22.6 MB | a composer holds two of the half-float ones: 45.2 MB1282 is the WebGL error code INVALID_OPERATION. Two targets taking turns (ping-pong) avoid it.
Console
Run the scene to see its console.Expected console output
a shows a: 1282 (1282 = INVALID_OPERATION: reading and writing one texture)
a shows b: 0
b shows a: 0 (two targets, taking turns: ping-pong)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
Forget to point drawing back at the canvas
In the first example delete the line renderer.setRenderTarget(null); // ...then back to the canvas. To see the error, add console.log("error:", renderer.getContext().getError()); before the console.log("target:" line.
Break #2
Size the target once and never again
In the second example delete the line target.setSize(size.x, size.y); // the fix: ....
Myth vs fact
Myth
Render targets are slow.
Fact
Drawing into one costs the same per pixel as drawing to the canvas. The cost is the memory and the extra full-screen draws.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
target.depthBufferis a renderbuffer unless you give the target aDepthTexture, which a later pass can sample (depth of field and ambient occlusion do). - ▸
By default a render target's texture has
NoColorSpace: three.js writes linear values into it, and tone mapping and the sRGB curve are skipped (they apply only when drawing to the canvas). That is why a half-float chain needs anOutputPassat the end (Topic 15.2).
Remember this
- 1
The problem this solves: until now every
render()call painted the canvas, and the canvas is the end of the road. Effects, mirrors, security-camera screens and minimaps all need the picture first and the final image second. Without a place to draw into that is not the screen, none of them can be built. - 2
What it is. A
WebGLRenderTarget(width, height)owns a colour texture (target.texture) and, by default, a depth buffer.renderer.setRenderTarget(target)points the nextrender()at it.renderer.setRenderTarget(null)points drawing back at the canvas. The first example draws a turning cube into a 256 × 256 target and shows it on a plane. - 3
The size is yours to keep. A target has a fixed size in real pixels. It does not follow the canvas. When the window grows, or
setPixelRatiochanges, a target that should match the screen must be resized withtarget.setSize(w, h). The second example shows a target that stays 300 × 150 on a 600 × 300 canvas, and the fix. - 4
What it costs. Memory is
width × height × bytes per pixel. A normal colour target is 8-bit RGBA, 4 bytes. A target that must hold values brighter than white (bloom, tone mapping) is half-float, 8 bytes. A full-screen target on a phone at pixel ratio 3 is 1170 × 2532 pixels: 22.6 MB in half-float, and effect chains hold two or more. This is why mobile pages slow down first. - 5
Limits and clean-up. A target cannot be larger than the GPU's
maxTextureSize(renderer.capabilities.maxTextureSize: the number differs per device: print it, never assume it), and a half-float target needs a GL extension that a few old GPUs lack (Topic 15.2 shows the check). A target is never freed by itself: when you no longer need it, calltarget.dispose()(Topic 17.4). If the GPU context is lost, a target's picture is gone until a frame draws into it again (Topic 18.2). - 6
Never read and write the same texture. A mirror that shows the picture it is being drawn into is a feedback loop. The GPU refuses it: WebGL raises
INVALID_OPERATION(error code 1282) and the draw is skipped. The third example shows the error and the fix: two targets that take turns (ping-pong).
Explain it without notes
What is a render target, and when would a showroom need one?
Why does a render target not resize with the canvas, and how do you keep them in step on a phone with pixel ratio 3?
Practice
Compute the memory of a half-float target for a 1366 × 768 screen at ratio 1, and of a chain holding two.
Write the lines that draw photoScene into target and then world to the canvas, restoring whatever target was active before.
Done when you can
Draw a scene into a
WebGLRenderTargetand show it as a textureCompute a target's memory from its size and pixel type
Keep a target in step with the canvas when the window or pixel ratio changes
Recognise a feedback loop (error 1282) and break it with two targets