Course outline
Pixels and Fill Rate
In one line
The GPU's work grows with the number of pixels it shades, and every transparent layer shades them again. Cap the pixel ratio, avoid stacked transparency, and draw only when the picture changed.
Think of it like this
Painting a wall. Doubling the wall's width and height means four times the paint. Painting five coats means five times the work, even if you never see the first four.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- pixel ratio
- Device pixels per CSS pixel. A canvas drawn at ratio 2 has 4 times the pixels of one at ratio 1.
- fill rate
- How many pixels per second the GPU can shade.
- overdraw
- Shading the same screen pixel several times, often because of stacked transparent layers.
- render on demand
- Drawing a new picture only when something changed, instead of every refresh.
- MSAA
- Multisample antialiasing: smooth edges by sampling several points per pixel at the edges, at a cost in work and memory.
Step by step
01See what the ratio costs
Run the first example: a 300 x 150 canvas at four ratios, then a phone-sized canvas at 1x, 2x, 3x and capped.
02Count layers
The second example turns transparent layers into a number you can read from one pixel.
03Stop drawing when nothing changes
The third example draws only when dirty is set. Then orbit the knot, let go, and watch the frames figure in the status bar stop growing.
04Decide a policy
Cap the ratio at 2 or lower, avoid large transparent sheets, draw on demand when nothing moves.
Try it yourself
- 1
Tighten the cap
In the first example, change
cap(3)tocap(3, 1.5). The row labelled "3x phone, capped at 2" now printsratio 1.5 -> 740,610 pixels per frame: a quarter of the 2,962,440 pixels at ratio 3. - 2
Stack more sheets
In the overdraw example, change
sheets(12)tosheets(40)in the "12 sheets" line. It printsdraw calls: 40 | layers blended over the middle pixel: 40: overdraw and draw calls grow together.
Code & diagrams
Sizes are computed from numbers, so the output is the same on every device.
Console
Run the scene to see its console.Expected console output
A 300 x 150 canvas:
pixel ratio 1 -> drawing buffer 300 x 150 = 45,000 pixels, 1 x the work of ratio 1
pixel ratio 1.5 -> drawing buffer 450 x 225 = 101,250 pixels, 2.25 x the work of ratio 1
pixel ratio 2 -> drawing buffer 600 x 300 = 180,000 pixels, 4 x the work of ratio 1
pixel ratio 3 -> drawing buffer 900 x 450 = 405,000 pixels, 9 x the work of ratio 1
A 390 x 844 phone viewport, full-screen canvas:
1x laptop screen ratio 1 -> 329,160 pixels per frame
2x phone ratio 2 -> 1,316,640 pixels per frame
3x phone ratio 3 -> 2,962,440 pixels per frame
3x phone, capped at 2 ratio 2 -> 1,316,640 pixels per frame
3x phone, capped at 1.5 ratio 1.5 -> 740,610 pixels per frameEach layer adds 1/255 of red. The value is read straight after render().
Console
Run the scene to see its console.Expected console output
1 sheet draw calls: 1 | layers blended over the middle pixel: 1
12 sheets draw calls: 12 | layers blended over the middle pixel: 12
12 sheets behind an opaque wall draw calls: 13 | layers blended over the middle pixel: 0The loop is driven by hand for 180 turns so the count is exact. The last two lines then start the real loop.
Console
Run the scene to see its console.Expected console output
60 frames, nobody touching anything: frames drawn = 1
after one change, 60 more frames: frames drawn = 2
with autoRotate on, 60 more frames: frames drawn = 61Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
A transparent wall hides nothing
In the overdraw example, change the wall's material to new THREE.MeshBasicMaterial({ color: "#000000", transparent: true, opacity: 0.5 }).
Myth vs fact
Myth
A sharper canvas is always better.
Fact
Beyond a ratio of about 2 the eye can rarely tell, but the GPU pays for every extra pixel. Cap it, and lower it further when the frame rate drops.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Dynamic resolution: watch the frame time, and when it stays over budget, lower
setPixelRatioa step (never below about 0.75); raise it again when there is room. It is the cheapest way to keep a heavy scene at a steady frame rate on weak phones.
Remember this
- 1
The problem this solves: a phone with a pixel ratio of 3 has nine device pixels for each CSS pixel. A full-screen canvas on such a screen makes the GPU shade nine times as many pixels as a standard screen, for a difference few people can see. It is a very common reason for a page to be GPU-bound on a good phone.
- 2
Pixel ratio. The first example prints the drawing buffer at four ratios: 1, 1.5, 2 and 3 are 1x, 2.25x, 4x and 9x the work. The standard cap,
Math.min(devicePixelRatio, 2), is already in the showroom'srenderer.js(Phase 3). A lower cap such as 1.5 saves nearly half again. Antialiasing (antialias: true) multiplies the work at the edges on top. - 3
Fill rate and overdraw. Fill rate is how many pixels the GPU can shade per second. Overdraw is shading the same screen pixel more than once. Opaque objects are sorted near to far, so the depth test throws away pixels hidden behind them. Transparent objects must be drawn far to near and blended, so each layer is shaded again.
- 4
Measure overdraw. The second example uses a shader that adds 1/255 of red per layer, so the red value of a pixel is the number of layers blended there. Twelve transparent sheets give 12 at the middle pixel (1 sheet gives 1). Put an opaque wall in front and it is 0: the sheets behind it are rejected by the depth test. Whether the GPU skips their shader as well is the hardware's choice, but the picture is guaranteed.
- 5
Half-resolution effects. A blur or glow does not need full sharpness. Render it into a target with half the width and height, and it shades a quarter of the pixels: at 1920 x 1080 that is 518,400 instead of 2,073,600. The postprocessing library's
KawaseBlurPassdefaults to aresolutionScaleof 0.5 for this reason (read in its 6.39.5 source). Phase 15's chain holds full-size half-float targets, so every full-screen effect there pays the whole pixel bill. - 6
Render on demand. A model viewer that nobody touches does not need 60 pictures a second. Draw only when something changed: the camera moved, the canvas resized, a colour was chosen. The third example counts the frames drawn in 60 loop turns: 1 when nothing happens, 2 after one change, 61 when something moves all the time (
autoRotate). Damping needs frames until it settles, socontrols.update()tells you when it is still moving. If a visitor asked for reduced motion (matchMedia("(prefers-reduced-motion: reduce)").matches), leaveautoRotateoff: the viewer then draws only on their own input.
Explain it without notes
Why is a phone at pixel ratio 3 often slower than a laptop, even with a faster chip?
How does an opaque object in front reduce the work, and why does a transparent one not?
Practice
Write a function pickRatio(devicePixelRatio, canvasWidth, canvasHeight, maxPixels) that lowers the ratio so the buffer never exceeds maxPixels.
A viewer with autoRotate freezes after you switch to render on demand. Why?
Done when you can
Say how the pixel ratio changes the work, and cap it
Explain overdraw, and why transparency causes it
Draw only when something changed, and keep animating things drawing
Lower the cost of a blur by rendering it at half size