Command Palette

Search for a command to run...

Hectal
Course outline
Topic 6.6Intermediate ~19 min · step 6 of 7

Texture Memory

In one line

A texture costs width × height × 4 bytes on the GPU, plus a third for mipmaps, whatever its file size. renderer.info.memory.textures counts them, and dispose() frees them.

0/7 · 0%

Think of it like this

A folded map in your pocket is thin. Unfolded on a table it is huge. A JPEG is the folded map; the GPU needs it unfolded, every pixel spelled out, so its size depends on the pixel count and not on how well the file folded.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

GPU memory
Memory the graphics chip reads from. On phones it is shared with everything else the device runs.
MiB
Mebibyte: 1,048,576 bytes. A 1024 × 1024 texture with 4 bytes per texel is exactly 4 MiB without mipmaps.
compressed texture
A format the GPU reads directly without unpacking, so it stays small in GPU memory. KTX2 files carry one.
dispose()
The method that deletes an object's GPU resources. It does not delete anything in JavaScript.

Step by step

01Compute before you load

For each texture: width × height × 4, times 4/3 with mipmaps. Add them up for the whole scene. For the front wheel: two 1024 textures are 10.7 MiB.

02Read the real numbers

The first example fetches each file's size and the picture's pixel size. The two columns show why the file size misleads: 343 KiB on disk is still 5.3 MiB on the GPU.

03Watch the count

The second example logs renderer.info.memory.textures before the first render (0), after it (6), after disposing three (3). The status bar of the live example shows the same count.

04Dispose what you drop

When a colour change replaces a texture, call old.dispose() after the swap. When a whole object is removed, dispose its geometry, materials and textures (Topic 17.4).

Try it yourself

  1. 1

    Compute a worse case

    In the first example add [8192, 8192] to the list of sizes. The line reads 256.00 MiB without mipmaps | 341.33 MiB with. One such texture is more than many phones allow a whole tab.

  2. 2

    Leak on purpose

    In the second example comment out the card.material.map.dispose(); line in the loop. The count after removing three cards stays at 6: removing a mesh from the scene does not free its textures.

Code & diagrams

Try it: interactive

UV viewer: the memory line

Read the GPU memory line while you switch mipmaps on and off. It is width × height × 4 bytes, times 4/3 with mipmaps, whatever the file size.

File size is not GPU size New tab

Real file sizes and pixel sizes of the course's textures, then the formula for common sizes. The last line is 300 × 200: odd sizes work, and cost what the formula says. A mipmap chain is about one third more (the exact value for odd sizes differs slightly).

idle

Console

Run the scene to see its console.

Expected console output

file                             on disk   on the GPU
metal_plate_diff_1k.jpg           676 KiB      5.3 MiB
metal_plate_nor_gl_1k.jpg         752 KiB      5.3 MiB
rubber_tiles_diff_1k.jpg          343 KiB      5.3 MiB
concrete_floor_02_diff_1k.jpg     758 KiB      5.3 MiB

512 x 512       1.00 MiB without mipmaps |    1.33 MiB with
1024 x 1024     4.00 MiB without mipmaps |    5.33 MiB with
2048 x 2048    16.00 MiB without mipmaps |   21.33 MiB with
4096 x 4096    64.00 MiB without mipmaps |   85.33 MiB with
300 x 200       0.23 MiB without mipmaps |    0.31 MiB with
Textures rising and falling with dispose() New tab

MeshBasicMaterial is used so the count starts at 0 (a Standard material adds one lookup table). The last two lines: a texture disposed while still in the scene comes back at the next render.

idle

Console

Run the scene to see its console.

Expected console output

created 6 textures, nothing drawn yet: 0
after the first render(): 6
removed 3 cards and disposed their textures: 3
disposed a texture that is still used: 2
after the next render(): 3 (it was uploaded again)

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

Swap textures without disposing

In the second example, right after the first renderer.render(scene, camera);, add a loop that runs 20 times: cards[0].material.map = new THREE.CanvasTexture(canvas); (a fresh 1024 × 1024 canvas each time) and renderer.render(scene, camera);, then print mem().

terminal
$ Run
── what you'll see ──
after the first render(): 6
after 20 swaps without dispose: 26
The count only goes up: 20 forgotten textures of about 5.3 MiB each is about 107 MiB. On a phone the page gets slow, then the tab reloads. Exercise 3 (Free the textures) lets you fix it.

Myth vs fact

Myth

A small JPEG is a cheap texture.

Fact

JPEG is a download format. The GPU needs every texel unpacked: a 340 KB photo of 1024 × 1024 takes 5.3 MiB; of 4096 × 4096 it takes 85 MiB.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    renderer.info.memory has only geometries and textures counts. There is no byte count: compute it yourself. Chrome's Task Manager (Shift + Esc on Windows and Linux) can show a GPU memory column per tab (right-click its header to add it), which catches leaks. HDR pictures cost more per texel: a half-float RGBA texel is 8 bytes, so the 1024 × 512 studio file is 4 MiB before it is prepared.

  • ▸

    Compressed GPU formats (ASTC on phones, BC7 on desktops) are about 1 byte per texel, a quarter of the 4 bytes; Basis Universal in KTX2 is transcoded at load time to whichever the device supports. They also cut upload time. The cost is some quality and a decoder (Topic 10.5).

Remember this

  1. 1

    The problem this solves: the story's crash. A 4096-pixel photo of the floor was 600 KB as a file and over 85 MiB on the GPU. A page that loads a handful of such pictures can use more memory than the phone allows, and the browser kills the tab. File size tells you the download time, not the memory cost.

  2. 2

    The rule. The GPU stores each texel as 4 bytes (red, green, blue, alpha), even for a JPEG without transparency. So bytes = width × height × 4. Mipmaps add about a third more (1/4 + 1/16 + ... = 1/3). A 2048 × 2048 texture with mipmaps: 2048 × 2048 × 4 = 16 MiB, × 4/3 = 21.3 MiB. The first example computes real files: each 1024 picture is about 5.3 MiB on the GPU, though the files are only 343 to 752 KiB.

  3. 3

    Size grows with the square. Doubling the width and height makes four times the memory: 1024 → 5.3 MiB, 2048 → 21.3 MiB, 4096 → 85.3 MiB. A 300 × 200 picture costs only 0.31 MiB. Forty textures (the scene of a busy configurator) at 1024 cost 40 × 5.33 = 213 MiB; at 2048 they cost 853 MiB, which kills a phone tab. Before shipping, ask how many screen pixels the texture will really cover; a bike part that fills 400 pixels does not need 4096.

  4. 4

    **renderer.info.memory.textures counts textures, not bytes.** It goes up when a texture is first used in a render and down when you call dispose(). Compute bytes yourself from the sizes. The second example creates six textures, renders (6), disposes three (3). One of them is still in a card, disposed by mistake: it is uploaded again at the next render. The status bar shows the same count.

  5. 5

    **What dispose() frees.** texture.dispose() deletes the GPU copy. Disposing a material or a geometry does not dispose its textures: textures are shared, so you must do it yourself. The copy in JavaScript memory goes when nothing refers to the texture any more. Remove it from the scene as well, or it is uploaded again.

  6. 6

    Ways to spend less. Resize pictures to the size they need; drop mipmaps for pictures always drawn at their own size; pack roughness, metalness and AO into one texture (ORM, Topic 6.4); share one texture between meshes; and use GPU-compressed textures (KTX2, Topic 10.5), which stay compressed on the GPU, roughly 4 to 8 times smaller. A phone gives a tab only part of the device's memory, and nothing in the browser tells you how much, so test on a real cheap phone, like Lena's.

Explain it without notes

01

A texture is a 300 KB JPEG at 2048 × 2048. How much GPU memory does it take, and how do you cut it?

02

What does renderer.info.memory.textures tell you, and what does it not?

Practice

01

Your showroom uses 12 textures of 2048 × 2048 with mipmaps. What is the total, and is it a problem?

02

Write the code that changes a mesh's colour map and frees the old one.

Done when you can

  • Compute a texture's GPU memory with and without mipmaps

  • Explain why file size does not predict memory

  • Read renderer.info.memory.textures and use it to find a leak

  • Free textures with dispose() and say what it does not free