Course outline
Pixels and the Canvas
In one line
A screen is a grid of coloured dots called pixels, and a web page draws 3D into a <canvas> element. The canvas has two sizes, the box it fills on the page and the grid of pixels it really draws, and when they disagree the picture is blurry.
Think of it like this
A mosaic on a wall is made of small coloured tiles. From across the room you see a picture; up close you see the tiles. A screen is a mosaic of tiny lit tiles called pixels. If a picture made with 100 tiles is stretched to cover a wall that needs 400, each tile has to be blown up, and the picture goes blocky and soft.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- pixel
- One tiny dot of a screen or image, with one colour. A picture is a grid of pixels.
- canvas
- An HTML element (
<canvas>) that holds a grid of pixels a program can draw into. - CSS pixel
- The unit used in CSS sizes such as
width: 200px. On a sharp screen one CSS pixel is made of several real pixels. - drawing buffer
- The grid of pixels the canvas really holds:
canvas.widthbycanvas.height. The browser stretches it to fit the CSS box. - device pixel ratio
- How many real screen pixels make one CSS pixel along one side: 1, 2 or 3 on most devices.
window.devicePixelRatio. - aspect ratio
- Width divided by height. A camera needs it to know how wide a picture to make.
Step by step
01The page asks for a size
You decide a CSS size, for example 200 × 100. renderer.setSize(200, 100) writes it into the canvas's style (200px, 100px). On its own this changes how big the canvas looks, not how many pixels it holds.
02The ratio multiplies it
setPixelRatio(2) tells three.js to hold twice as many pixels in each direction. Now canvas.width is 400 and canvas.height is 200: 80,000 pixels instead of 20,000. The browser squeezes the 400 × 200 buffer into the 200 × 100 box, so each CSS pixel shows 4 buffer pixels.
03The GPU fills the buffer
When you call render(scene, camera), the GPU works out a colour for each of the 80,000 pixels. Double the pixel ratio and it does four times the colouring. This is why a phone at ratio 3 can feel slower than a laptop at ratio 1 in the same scene.
04The browser stretches the result
The browser places the finished buffer on the page and scales it to the CSS box. If the buffer is smaller than the box it is enlarged and looks blurry. If it is larger, as at ratio 2, it is shrunk and looks sharp on a screen that really has that many pixels.
Try it yourself
- 1
Bigger box, bigger bill
In "Same box, three pixel ratios", change
CSS_Wto 300 andCSS_Hto 150. The lines now readbuffer 300 x 150 = 45000 pixels,600 x 300 = 180000 pixelsand900 x 450 = 405000 pixels. Ratio 3 is always 9 times ratio 1, whatever the box. - 2
See the blocks
In "A blurry canvas on purpose", change
make("blurry", 38, 38)tomake("blurry", 10, 10). The right cube becomes a few big squares, and the console says each buffer pixel covers 15.0 × 15.0 CSS pixels.
Code & diagrams
The first three lines come from throwaway renderers. The scene you see uses a ratio capped at 2. Look at the status bar: the pixel ratio is shown there.
Console
Run the scene to see its console.Expected console output
ratio 1: style 200px x 100px | buffer 200 x 100 = 20000 pixels
ratio 2: style 200px x 100px | buffer 400 x 200 = 80000 pixels
ratio 3: style 200px x 100px | buffer 600 x 300 = 180000 pixelsBoth canvases fill a 150 × 150 box. The right one has only 38 × 38 pixels, so each of its pixels covers about 4 × 4 screen pixels.
Console
Run the scene to see its console.Expected console output
sharp: buffer 150 x 150 -> each buffer pixel covers 1.0 x 1.0 CSS pixels (in its 150 x 150 box)
blurry: buffer 38 x 38 -> each buffer pixel covers 3.9 x 3.9 CSS pixels (in its 150 x 150 box)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
Leave the CSS size alone
In "Same box, three pixel ratios", change r.setSize(CSS_W, CSS_H); to r.setSize(CSS_W, CSS_H, false); (a third argument, false).
Myth vs fact
Myth
A bigger canvas is always a better picture.
Fact
More pixels give a sharper picture only up to what the screen can show. Beyond that you pay in speed and battery for detail nobody sees. Cap the ratio at 2.
Myth
window.innerWidth is the number of pixels on the screen.
Fact
It is the width in CSS pixels. On a phone with a ratio of 3 the real screen is three times wider than that number.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Browsers limit how large a canvas can be (the limit depends on the browser and the GPU, and is often around 16,000 pixels on a side or a total area limit). A canvas over the limit comes out blank. Always cap
width × ratiofor very large displays. - ▸
The status bar under every live canvas shows the pixel ratio. When a scene is slow on a phone, the first experiment is
renderer.setPixelRatio(1): if the frame rate jumps, the scene is fill-rate bound (limited by how many pixels it must colour), not triangle bound. Phase 17 teaches this properly. - ▸
antialias: trueasks for MSAA, which colours the edges of triangles at several points per pixel. It costs memory and time in proportion to the pixel count, so it matters more on a ratio 1 screen than on a ratio 3 screen, where pixels are already tiny.
Remember this
- 1
The problem this solves: the most common first complaint about 3D on the web is "it looks blurry" or "it is slow on my phone". Both come from the same fact: the canvas has a size on the page and a different size in pixels, and you must choose both on purpose.
- 2
The canvas. A
<canvas>is an HTML element that is a blank rectangle of pixels your code can paint on. three.js'sWebGLRenderermakes one for you (renderer.domElement) and asks the GPU to fill it. A canvas that is never given a size is 300 × 150 pixels, the browser's default. - 3
Two sizes. The CSS size is how big the canvas looks on the page (its
style.widthandstyle.height, in CSS pixels). The drawing-buffer size is how many real pixels it holds (canvas.widthandcanvas.height). The browser stretches the buffer to fill the CSS box. When the buffer has fewer pixels than the box, each buffer pixel covers several screen pixels and you see blur or blocks. - 4
Device pixel ratio. A modern phone screen packs 2 or 3 real pixels into each CSS pixel, so text and pictures are sharp. That number is
window.devicePixelRatio. To be as sharp as the screen, the buffer needs CSS size × ratio pixels in each direction.renderer.setPixelRatio(r)sets the ratio andrenderer.setSize(w, h)sets the CSS size; the buffer becomesw * rbyh * r. - 5
The cost. The GPU must colour every pixel of the buffer on every frame. Ratio 2 means 4 times the pixels of ratio 1; ratio 3 means 9 times. The first live example prints this. That is why the showroom uses
Math.min(window.devicePixelRatio, 2): ratio 3 costs 2.25 times as much as ratio 2 for a gain most eyes can't see. - 6
Resizing. The canvas does not resize itself. When the window changes (or a phone turns), you must call
setSizeagain and update the camera'saspect(width ÷ height) withupdateProjectionMatrix(), or the picture is squashed. A container with no height gives a canvas of height 0 and nothing to see; Topic 0.6 showed that.
Explain it without notes
A friend says their 3D scene looks blurry only on their phone. Give two causes and how you would check each.
Why does setPixelRatio(3) slow a scene down even though nothing in the scene changed?
Practice
A card on a web page is 320 × 180 CSS pixels. What buffer size does a ratio-2 renderer need, and how many pixels is that? Write the two lines of code.
Write the one line that gives the showroom a sharp but affordable picture on any device.
Done when you can
Say what a pixel and a canvas are
Tell the CSS size from the drawing-buffer size and compute one from the other
Explain why pixel ratio 3 costs 9 times the work of ratio 1
Say why the showroom caps the ratio at 2