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
Three.js documentation
GLSL functions
Others
OGLDEV (Youtube)
Shortcuts ⌨️
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
When it comes to shaders, lighting is a very difficult topic that solely justifies the use of Three.js.
Implementing lights in Three.js is as simple as instantiating a class such as DirectionalLight or PointLight and adding that instance to the scene.
But behind those pre-made classes are very complex algorithms trying to recreate real-life lighting using complex physically-based formulas and I’m not even talking about how the light settings are injected into the shaders.
What a relief not to have to write those algorithms, right? Well, guess what? We are going to do it ourselves.
You might wonder why would we inflict such pain on ourselves and there are multiple reasons for it.
Firstly, for the sake of learning. The various formulas and concepts we are going to discover will step up your shader game and can be used in many other topics.
Secondly, you don’t always need Three.js built-in lights. If you create a custom shader and want to add a simple shade so that it feels like there is some light coming from the side, good luck with implementing Three.js lights into your custom shader.
Thirdly, to create cool effects as we are going to see in the next lessons.
We are not going to push the concept as far as Three.js built-in lights, but we are going to create some of the most common lights which are the point light, the directional light, and the ambient light:
Also, note that the implementation we are going to do is not physics-based. It’s more of an old-school and performant approach, quite similar to the Phong shading with minor tweaks.
Most of the calculations will be done in the fragment shader in order to avoid visual artifacts. Yet, it could be done easily in the vertex shader, which is actually what we call Gouraud shading.
Setup 03:20
The starter already contains the following:
- 3 rotating objects: a sphere, Suzanne, and a torus knot
- A single instance of ShaderMaterial for all 3 objects
- A basic shader already featured in the
src/shaders/shading/
folder (I wasn’t inspired) with avertex.glsl
and afragment.glsl
- An instance of
lil-gui
with acolor
tweak that controls theuColor
uniform is sent to the fragment shader. - The
vite-plugin-glsl
dependency to handle GLSL files OrbitControls
to rotate around
Ambient light 05:03
Let’s start with the simplest of the three and create an ambient light. It’s not the most impactful one, but starting with it will help us understand the structure we are going to adopt and how we combine the light we calculate with the initial color of the material.
An ambient light applies a uniform light on the surface of the objects, regardless of their orientation. It’s not realistic, but in small doses, it helps lighten up the part of the objects in the shade as if the light was bouncing on walls and getting back to the object.
We are going to put all the code related to the light in a separate function from the start.
In the fragment shader, before the main()
, create an ambientLight
function returning a vec3
:
vec3 ambientLight()
{
return vec3(0.0, 0.0, 0.0);
}
void main()
{
vec3 color = uColor;
// Final color
gl_FragColor = vec4(color, 1.0);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
Using a separate function is good, but not just for other projects. By doing so, we will be able to add more lights just by calling the function again. We will demonstrate that later in the lesson.
We are returning a vec3
because light can be colored, and currently, it seems that the light is off since we set it to 0.0, 0.0, 0.0
.
This might sound silly, but it’s actually the perfect opportunity to understand how we should combine the light with the original color of the object.
In main()
, right after creating the color
variable, create a light
variable to vec3(0.0)
and add the output of ambientLight()
to it using +=
:
void main()
{
vec3 color = uColor;
// Lights
vec3 light = vec3(0.0);
light += ambientLight();
// ...
}
light
will contain the various light info we are going to calculate, starting with ambientLight()
. But how do we combine it with the color
?
Here’s a clue. In real life, if there is no light, what should be the perceived color of the objects? Black, indeed.
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