SPACE
to play / pauseARROW RIGHT
orL
to go forwardARROW LEFT
orJ
to go backwardARROW UP
to increase volumeARROW DOWN
to decrease volumeF
to toggle fullscreenM
to toggle mute0 to 9
to go to the corresponding part of the videoSHIFT
+,
to decrease playback speedSHIFT
+.
or;
to increase playback speed
Shortcuts ⌨️
⚠️ Update
Use version 3.12
:
npm install --save gsap@3.12
Unlock content 🔓
To get access to 93 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? 🤘
That's the end of the free part 😔
To get access to 93 hours of video, a members-only Discord server and future updates, join us for only $95!
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.
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