Command Palette

Search for a command to run...

Hectal
Course outline
Topic 15.1Advanced ~21 min · step 1 of 6

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.

0/6 · 0%

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. 1

    Shrink the picture

    In the first example change 256, 256 to 64, 64. The console prints target: 64 x 64 and size of the picture: 16384 bytes, and the cube on the screen turns blocky and soft: 64 pixels are stretched over the plane.

  2. 2

    Match the pixel ratio

    In the second example change setPixelRatio(2) to setPixelRatio(3). The fixed: line now reads canvas 1800 x 900 | target 1800 x 900: the same 600 × 300 box holds nine times the pixels of ratio 1.

Code & diagrams

A turning cube on a screen inside the scene New tab

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.

idle

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)
A target that did not resize, and what targets cost New tab

Sizes are printed in real pixels. The last two lines are plain arithmetic: width × height × bytes per pixel.

idle

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 MB
Reading and writing the same texture New tab

1282 is the WebGL error code INVALID_OPERATION. Two targets taking turns (ping-pong) avoid it.

idle

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.

terminal
$ Run
── what you'll see ──
error: 1282
(the canvas stays blank: nothing is ever drawn onto it)

Break #2

Size the target once and never again

In the second example delete the line target.setSize(size.x, size.y); // the fix: ....

terminal
$ Run
── what you'll see ──
fixed: canvas 1200 x 600 | target 300 x 150

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.depthBuffer is a renderbuffer unless you give the target a DepthTexture, 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 an OutputPass at the end (Topic 15.2).

Remember this

  1. 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. 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 next render() 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. 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 setPixelRatio changes, a target that should match the screen must be resized with target.setSize(w, h). The second example shows a target that stays 300 × 150 on a 600 × 300 canvas, and the fix.

  4. 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. 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, call target.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. 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

01

What is a render target, and when would a showroom need one?

02

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

01

Compute the memory of a half-float target for a 1366 × 768 screen at ratio 1, and of a chain holding two.

02

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 WebGLRenderTarget and show it as a texture

  • Compute 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