Command Palette

Search for a command to run...

Hectal
Course outline
Topic 6.1Intermediate ~20 min · step 1 of 7

Loading Textures

In one line

A texture is a picture the GPU reads. TextureLoader fetches it, but the picture arrives later, it can fail, and a colour picture needs one extra line (colorSpace) or it looks washed out.

0/7 · 0%

Think of it like this

You order wallpaper for a model house. The roll comes in the post, so there is a wait, and sometimes the parcel never arrives or holds the wrong thing. When it does arrive, you must say whether it is a photo (colours made for eyes) or a map of numbers. The builder reads them in different ways.

Words you'll meet

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

texture
A picture the GPU reads to decide the colour (or another value) at each point of a surface.
texel
One pixel of a texture. It is called a texel to keep it apart from a pixel of the screen.
sRGB
The colour encoding of photos and screens. It stores dark shades finely. A picture in sRGB must be converted before light is computed.
linear
Numbers that are real amounts of light: twice the number, twice the light. Lighting maths needs this.
CORS
The browser rule that lets one website read another website's files only if that site allows it with a header.

Step by step

01Make a loader and ask for the file

const loader = new THREE.TextureLoader() makes the loader. await loader.loadAsync(url) starts the download and waits. Paths starting with / are relative to your site's root, so /data/3d/textures/x.jpg is the same file on this page and in your project's public/ folder.

02Decide what the picture is

A colour picture (the surface's map, an emissive map) gets colorSpace = THREE.SRGBColorSpace. Everything else (normal, roughness, metalness, AO) is data and keeps the default.

03Put it on a material

material.map = texture. If the material was already drawn without a map, add material.needsUpdate = true once: the shader must be rebuilt to read a texture.

04Plan for failure

Wrap the load in try / catch. Decide the fallback before you need it: a plain colour, the previous texture, or a message in #status (Phase 10).

Try it yourself

  1. 1

    Skip the colour-space line

    In the first example delete texture.colorSpace = THREE.SRGBColorSpace;. The concrete turns lighter and flatter, because the photo's sRGB numbers are read as linear light. Put the line back and the stone darkens.

  2. 2

    Load a missing file

    In the first example change the file name to concrete_floor_03_diff_1k.jpg. The code stops at await loadAsync: the console shows an uncaught #<Event> error (an Event, not an Error), the slab stays plain grey, and nothing after the before load line prints. Wrap the load in try / catch to keep the rest alive.

Code & diagrams

Load, upload, and the colour-space line New tab

The slab turns while you look at the status bar: the texture count rises only after the first frame. Concrete photo: Poly Haven, CC0.

idle

Console

Run the scene to see its console.

Expected console output

before load: textures in GPU memory = 1
loaded: true | size: 1024 x 1024
colorSpace right after load: ""
textures in GPU memory (not used yet): 1
textures in GPU memory after the first frame: 2
Four ways a load can go (and one that returns too early) New tab

Real results from this page: a wrong folder, a file that is not a picture and another website all reject with an Event of type error. The last line shows load() returning before the picture exists.

idle

Console

Run the scene to see its console.

Expected console output

right path    | ok: 1024x1024
wrong folder  | failed: Event error
not an image  | failed: Event error
other website | failed: Event error
load() returns at once | image so far: null | isTexture: true
One grey texel, two colour spaces New tab

Both tiles use the same texel, 128 out of 255. The left one is told it is sRGB and shows 128. The right one is not told, and the maths treats 128 as a true amount of light: it shows 188, a washed-out look. The numbers are read back from the canvas.

idle

Console

Run the scene to see its console.

Expected console output

SRGBColorSpace  texel 128 -> screen 128
NoColorSpace    texel 128 -> screen 188

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

Render before the picture arrives

Use const texture = loader.load(url) (no await) and render at once, as the last line of the second example does.

terminal
$ Run
── what you'll see ──
load() returns at once | image so far: null | isTexture: true
A mesh using this texture is drawn black (pixel 0, 0, 0) until the picture arrives, then changes to the real colour on its own.

Myth vs fact

Myth

A failed texture throws an Error with a message I can read.

Fact

It rejects with a plain Event (type: "error"). The real cause is in the browser's network panel: the status code (404), or a CORS message.

Pro corner

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

  • ▸

    A texture bigger than the GPU's maxTextureSize is not refused: three.js shrinks it with a canvas and warns Texture has been resized from (WxH) to (WxH). Read the limit from renderer.capabilities.maxTextureSize (4096 on some phones, 16384 on many desktops) and never ship a bigger picture than your smallest target.

  • ▸

    For 8-bit images in sRGB, three.js uploads an sRGB format, so the GPU converts to linear while sampling, for free and with correct blending of neighbouring texels. The format is chosen at upload, so set colorSpace before the first render. Measured: changing it afterwards does nothing (the screen keeps 188 for the grey texel) until texture.needsUpdate = true uploads the texture again.

Remember this

  1. 1

    The problem this solves: textures are the first thing in this course that comes from outside your code. Outside things are slow, they fail, and they have a meaning three.js cannot guess. Without a plan you get a black plane, a washed-out photo, or a crash on a phone.

  2. 2

    **A texture is a Texture object that wraps an image.** new THREE.TextureLoader() fetches the file; loader.loadAsync(url) returns a promise that resolves with the texture once the picture has arrived. loader.load(url) returns a texture at once with image still null: the mesh draws black until the picture arrives (measured: the pixel in the middle of a plane reads 0, 0, 0 before, and the photo's colour after). Prefer await loadAsync, so the code after it knows the picture is there.

  3. 3

    Nothing is on the GPU until it is used. Loading only decodes the picture into memory. The upload to the GPU happens at the first render() that draws a material using the texture. Open the status bar's texture count in the first example: it stays the same after the load and rises after the first frame. (A scene with a Standard material starts at 1: three.js r186 keeps one small lookup table texture for its lighting.)

  4. 4

    **Failure is an Event, not an Error.** If the file is missing, is not an image, or the server refuses it, the promise rejects with an Event whose type is "error"; there is no message and no status code. Use try / catch and say what you tried to load. The second example shows four cases. One more trap: a wrong letter case (Rubber.jpg for rubber.jpg) works on a Windows dev machine and returns 404 on a Linux server.

  5. 5

    CORS is the failure that only happens on other websites. An image from another origin loads only if that server sends an Access-Control-Allow-Origin header. three.js asks for it (crossOrigin = "anonymous"). If the header is missing the browser blocks the picture and logs Access to image at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. (the page's own files and files in public/ never have this problem). The preview here has no network, so this page cannot show it live; the words above come from a real run in Chromium on two local servers.

  6. 6

    **colorSpace says what the numbers mean. A photo stores colours the way screens show them (sRGB, which spends more numbers on dark shades). The lighting maths works on true amounts of light (linear). So for a colour** map you set texture.colorSpace = THREE.SRGBColorSpace, and three.js converts on the way in. TextureLoader leaves it as NoColorSpace, which means "treat the numbers as they are". That is right for data maps (Topic 6.4) and wrong for a photo: the third example shows a grey texel of 128 appearing as 188.

Explain it without notes

01

A friend's texture is washed out and pale. What is the likely cause, and what is the one-line fix?

02

Why does loadAsync reject with an Event, and how do you find the real reason for a failed texture?

Practice

01

Write an async function loadColour(url) that returns a ready colour texture, or null when the file fails, and logs the failing URL.

02

A plane shows a photo that looks too bright. What do you check first?

Done when you can

  • Load a texture with await loadAsync and say when it reaches the GPU

  • Set colorSpace correctly for colour and for data maps

  • Catch a failed load and say where the real reason is

  • Explain CORS in one sentence and why local files never meet it