00:00/00:00TIME LEFT
Choosing the a bcd

Shortcuts ⌨️

  • SPACE to play / pause
  • ARROW RIGHT or L to go forward
  • ARROW LEFT or J to go backward
  • ARROW UP to increase volume
  • ARROW DOWN to decrease volume
  • F to toggle fullscreen
  • M to toggle mute
  • 0 to 9 to go to the corresponding part of the video
  • SHIFT + , to decrease playback speed
  • SHIFT + . or ; to increase playback speed

Want to learn more? 👍


That's the end of the free part 😔

To get access to 71 hours of video, a members-only Discord server and future updates, join us for only $95!

Next lesson


Difficulty Hard

Starter packFinal project

Introduction 00:00

Bored with your red cube yet? It's time to add some textures.

But first, what are textures and what can we really do with them?

What are textures? 00:16

Textures, as you probably know, are images that will cover the surface of your geometries. Many types of textures can have different effects on the appearance of your geometry. It's not just about the color.

Here are the most common types of textures using a famous door texture by João Paulo. Buy him a Ko-fi or become a Patreon if you like his work.

Color (or albedo)

The albedo texture is the most simple one. It'll only take the pixels of the texture and apply them to the geometry.


The alpha texture is a grayscale image where white will be visible, and black won't.


The height texture is a grayscale image that will move the vertices to create some relief. You'll need to add subdivision if you want to see it.


The normal texture will add small details. It won't move the vertices, but it will lure the light into thinking that the face is oriented differently. Normal textures are very useful to add details with good performance because you don't need to subdivide the geometry.

Ambient occlusion

The ambient occlusion texture is a grayscale image that will fake shadow in the surface's crevices. While it's not physically accurate, it certainly helps to create contrast.


The metalness texture is a grayscale image that will specify which part is metallic (white) and non-metallic (black). This information will help to create reflection.


The roughness is a grayscale image that comes with metalness, and that will specify which part is rough (white) and which part is smooth (black). This information will help to dissipate the light. A carpet is very rugged, and you won't see the light reflection on it, while the water's surface is very smooth, and you can see the light reflecting on it. Here, the wood is uniform because there is a clear coat on it.


Those textures (especially the metalness and the roughness) follow what we call PBR principles. PBR stands for Physically Based Rendering. It regroups many techniques that tend to follow real-life directions to get realistic results.

While there are many other techniques, PBR is becoming the standard for realistic renders, and many software, engines, and libraries are using it.

For now, we will simply focus on how to load textures, how to use them, what transformations we can apply, and how to optimize them. We will see more about PBR in later lessons, but if you're curious, you can learn more about it here:

How to load textures 10:45

Getting the URL of the image

To load the texture, we need the URL of the image file.

Because we are using a build tool, there are two ways of getting it.

You can put the image texture in the /src/ folder and import it like you would import a JavaScript dependency:

import imageSource from './image.png'


Or you can put that image in the /static/ folder and access it just by adding the path of the image (without /static) to the URL:

const imageSource = '/image.png'


Be careful, this /static/ folder only works because of the Vite template's configuration. If you are using other types of bundler, you might need to adapt your project.

We will use the /static/ folder technique for the rest of the course.

Loading the image

You can find the door textures we just saw in the /static/ folder, and there are multiple ways of loading them.

Using native JavaScript

With native JavaScript, first, you must create an Image instance, listen to the load event, and then change its src property to start loading the image:

const image = new Image()
image.onload = () =>
    console.log('image loaded')
image.src = '/textures/door/color.jpg'

You should see 'image loaded' appears in the console. As you can see, we set the source to '/textures/door/color.jpg' without the /static folder in the path.

We cannot use that image directly. We need to create a Texture from that image first.

This is because WebGL needs a very specific format that can be access by the GPU and also because some changes will be applied to the textures like the mipmapping but we will see more about that a little later.

Create the texture with the Texture class:

const image = new Image()
image.addEventListener('load', () =>
    const texture = new THREE.Texture(image)
image.src = '/textures/door/color.jpg'

What we need to do now is use that texture in the material. Unfortunately, the texture variable has been declared in a function and we can not access it outside of this function. This is a JavaScript limitation called scope.

We could create the mesh inside the function, but there is a better solution consisting on creating the texture outside of the function and then updating it once the image is loaded by setting the texture needsUpdate property to true:

const image = new Image()
const texture = new THREE.Texture(image)
image.addEventListener('load', () =>
    texture.needsUpdate = true
image.src = '/textures/door/color.jpg'

While doing this, you can immediately use the texture variable immediately, and the image will be transparent until it is loaded.

To see the texture on the cube, replace the color property by map and use the texture as value:

const material = new THREE.MeshBasicMaterial({ map: texture })

You should see the door texture on each side of your cube.

Using TextureLoader

The native JavaScript technique is not that complicated, but there is an even more straightforward way with TextureLoader.

Instantiate a variable using the TextureLoader class and use its .load(...) method to create a texture:

const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')

Internally, Three.js will do what it did before to load the image and update the texture once it's ready.

You can load as many textures as you want with only one TextureLoader instance.

You can send 3 functions after the path. They will be called for the following events:

  • load when the image loaded successfully
  • progress when the loading is progressing
  • error if something went wrong
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
    () =>
        console.log('loading finished')
    () =>
        console.log('loading progressing')
    () =>
        console.log('loading error')

If the texture doesn't work, it might be useful to add those callback functions to see what is happening and spot errors.

Using the LoadingManager

Finally, if you have multiple images to load and want to mutualize the events like being notified when all the images are loaded, you can use a LoadingManager.

Create an instance of the LoadingManager class and pass it to the TextureLoader:

const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(loadingManager)

You can listen to the various events by replacing the following properties by your own functions onStart, onLoad, onProgress, and onError:

const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = () =>
    console.log('loading started')
loadingManager.onLoad = () =>
    console.log('loading finished')
loadingManager.onProgress = () =>
    console.log('loading progressing')
loadingManager.onError = () =>
    console.log('loading error')

const textureLoader = new THREE.TextureLoader(loadingManager)

You can now start loading all the images you need:

// ...

const colorTexture = textureLoader.load('/textures/door/color.jpg')
const alphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const heightTexture = textureLoader.load('/textures/door/height.jpg')
const normalTexture = textureLoader.load('/textures/door/normal.jpg')
const ambientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const metalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const roughnessTexture = textureLoader.load('/textures/door/roughness.jpg')

As you can see here, we renamed the texture variable colorTexture so don't forget to change it in the material too:

const material = new THREE.MeshBasicMaterial({ map: colorTexture })

The LoadingManager is very useful if you want to show a loader and hide it only when all the assets are loaded. As we will see in a future lesson, you can also use it with other types of loaders.

Want to learn more?

That's the end of the free part 😔

To get access to 71 hours of video, a members-only Discord server and future updates, join us for only $95!

How to use it 🤔

  • Download the Starter pack or Final project
    (if your browser detects a menace, do not worry, it is not)
  • Unzip it
  • Open your terminal and go to the unzip folder
  • Run npm install to install dependencies
    (if your terminal warn you about vulnerabilities, ignore it)
  • Run npm run dev to launch the local server
    (your browser should start automatically)
  • Start coding
  • The JS is located in src/script.js
  • The HTML is located in src/index.html
  • The CSS is located in src/style.css

If you get stuck and need help, join the members-only Discord server: