00:00/00:00
3:22
00:03:22

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

⚠️ Update

Use version 3.12:

npm install --save gsap@3.12

Unlock content 🔓

To get access to 91 hours of video, a members-only Discord server, subtitles, lesson resources, future updates and much more join us for only $95!

Want to learn more? 👋

65%

That's the end of the free part 😔

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

Next lesson
47.

Intro and loading progress

Difficulty Medium

Introduction 00:00

Until now, all we had was a WebGL canvas on our page with things showing up once they are ready.

In this lesson, we will learn how to add a very simple loader composed of a bar that fills while the assets are loading. The whole scene will be black and only show once everything is loaded with a nice fade.

For the loader, we will use HTML and CSS. That is an excellent opportunity to see how to combine HTML with WebGL.

Setup 01:01

Our starter contains what we did in the Realistic Render lesson with the Flight Helmet.

Overlay 01:22

First, we need a way to fade the scene. There are many ways of doing so. We could animate the <canvas>'s CSS opacity. We could also put a black <div> above the <canvas> and animate its CSS opacity. But instead, we are going to keep things inside the WebGL and draw a black rectangle that covers the whole render and fade it out when we need it.

The problem is: how do we draw a rectangle in front of the camera. With the knowledge we have now, we could create a plane and put it right inside of the camera instead of the scene, it should work fine because camera inherit from Object3D, but it looks a bit patched up.

Instead, we will draw a plane that doesn't follow the rules of position, perspective, and projection so that it just get drawn in front of the view. Don't worry; it's easier than what you might think.

Base plane

First, we are going to start from a classic plane.

Create a PlaneGeometry, a MeshBasicMaterial and a Mesh. Then add it all to the scene:

/**
 * Overlay
 */
const overlayGeometry = new THREE.PlaneGeometry(1, 1, 1, 1)
const overlayMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial)
scene.add(overlay)

The plane should be visible from the other side of the helmet.

Fill the render

We now want this plane to be always in front of the camera. We want it to fill the render regardless of the camera position. To do that, we are going to use a ShaderMaterial.

Replace the MeshBasicMaterial by a ShaderMaterial and write the default shaders that we have learned previously with the vertexShader property and the fragmentShader property. You can try to do this from memory but don't be frustrated if you can't. It takes time:

const overlayMaterial = new THREE.ShaderMaterial({
    vertexShader: `
        void main()
        {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
})

You should get the same result, but this time, we have control over the shaders.

To make the plane fill the render, we need to not apply the matrices:

const overlayMaterial = new THREE.ShaderMaterial({
    vertexShader: `
        void main()
        {
            gl_Position = vec4(position, 1.0);
        }
    `,
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `
})

Now you get a big rectangle in the middle. Neither its position, the camera position, the field of view or anything else transform it because none of the matrices are used.

The coordinates of the plane's vertices go from -0.5 to +0.5 because our plane has a size of 1.

The vertex shader, stripped like that, draws the triangles on the screen in the specified coordinates without considering anything else. We can see these triangles by setting the wireframe property to true:

const overlayMaterial = new THREE.ShaderMaterial({
    wireframe: true,
    // ...
})

Comment or remove the wireframe.

To get a bigger rectangle, we need the coordinates to go from -1 to +1. To do that, double the size of the PlaneGeometry:

const overlayGeometry = new THREE.PlaneGeometry(2, 2, 1, 1)

The rectangle is now filling the whole render.

Color and alpha

Let's say instead of this red color, we want black.

Change the gl_FragColor:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
        }
    `
})

Everything appears to be black.

Now, we want to be able to control the alpha. As you know, the fourth value of the gl_FragColor.

Set this fourth parameter to 0.5 to see if it's working:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    fragmentShader: `
        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, 0.5);
        }
    `
})

Unfortunately, everything is still fully black, and it's because we forgot an easy to forget a thing. We need to set the transparent property to true on our ShaderMaterial:

const overlayMaterial = new THREE.ShaderMaterial({
    transparent: true,
    // ...
})

The whole scene should look darker.

Uniform

Now that we have our overlay set, we need a way to control the alpha value. We are going to use a uniform.

Add a uAlpha uniform as we did before:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    uniforms:
    {
        uAlpha: { value: 0.5 }
    },
    // ...
})

Then use it in the fragmentShader instead of the raw 0.5:

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    fragmentShader: `
        uniform float uAlpha;

        void main()
        {
            gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha);
        }
    `
})

You should get the same result, but we can control the alpha directly from the JavaScript with the uAlpha uniform this time.

Let's change the value of that uniform to 1 to start with an entirely black screen.

const overlayMaterial = new THREE.ShaderMaterial({
    // ...
    uniforms:
    {
        uAlpha: { value: 0.5 }
    },
    // ...
})

Loading

Now that we have our overlay ready to be animated, we want to know when everything is loaded.

While there is only one model in the scene, we are genuinely loading many assets. We are loading the 6 images that compose the environment map, the model's geometries, and all the textures used in the model.

To load these assets, we used a GLTFLoader and a CubeTextureLoader. Both can receive a LoadingManager as parameter. That LoadingManager —as we saw at the beginning of the course— can be used to stay informed of the global loading progress.

Instantiate a LoadingManager and use it in the GLTFLoader and CubeTextureLoader:

/**
 * Loaders
 */
const loadingManager = new THREE.LoadingManager()
const gltfLoader = new GLTFLoader(loadingManager)
const cubeTextureLoader = new THREE.CubeTextureLoader(loadingManager)

Nothing should have changed, but we can now send two functions to the LoadingManager.

The first one will be triggered when everything is loaded, and the second one will be trigger when the loading progress.

Add these two functions with the following parameters:

const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        console.log('loaded')
    },

    // Progress
    () =>
    {
        console.log('progress')
    }
)

You should get multiple "progress" in the logs and one "loaded" at the end.

The progress function will be helpful later. For now, all we need is the loaded function.

Animate

To fade out the overlay, we need a way to animate the uAlpha uniform value. While this is a little far-fetched, just for that, we will use the GSAP library as we did at the beginning of the course.

First, in the terminal, install the gsap library with npm install --save gsap@3.12 —relaunch the server if you stopped it.

Now that we have gsap in the dependencies, we can import it:

import { gsap } from 'gsap'

Finally, we can use it to animate the uAlpha uniform value in the loaded function:

const loadingManager = new THREE.LoadingManager(
    // Loaded
    () =>
    {
        gsap.to(overlayMaterial.uniforms.uAlpha, { duration: 3, value: 0 })
    },

    // ...
)

The overlay should fade out nicely once everything is loaded.

Want to learn more?

That's the end of the free part 😔

To get access to 91 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
  • Unzip it
  • Open your terminal and go to the unzip folder
  • Run npm install to install dependencies
    (if your terminal warns you about vulnerabilities, ignore it)
  • Run npm run dev to launch the local server
    (project should open on your default browser 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:

Discord