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

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!

Next lesson

Lights Shading

Difficulty Very hard

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 a vertex.glsl and a fragment.glsl
  • An instance of lil-gui with a color tweak that controls the uColor 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.

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!

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: