WebGL and Three.js: Lighting

Written by Greg Stier
on April 23, 2014

What is WebGL and Three.js?

WebGL is a Javascript API used to render 3D graphics to the screen in a browser. Programming directly in the WebGL API can be complicated and messy, but lucky for us there are libraries that simplify this. One such library is Three.js.

Three.js

Three.js is a lightweight 3D library that hides a lot of the WebGL complexities and makes it very simple to get started with 3D programming on the web. Three.js can be downloaded from github or the three.js website, where you will also find links to documentation and examples.

In my first tutorial, I showed you how to setup a basic Three.js application. In that tutorial, I talked about setting the stage, building a scene, and animating a simple cube. In my second tutorial, I explained texture mapping and showed three different ways to texture objects using Three.js.

In this tutorial I’m going to go over lighting in Three.js. I’m going to explain the following five different types of lights and how to use each one: directional lights, ambient lights, point lights, spot lights, and hemisphere lights.

To view this demonstration, you will need a WebGL compatible browser. If you have a recent version of any major browser, you should be able to view it. If you are running an older version of Internet Explorer or viewing this on a mobile device, you may be out of luck. You can view the result of this tutorial here, and the source code can be found here.

The Art of Lighting

Most people don’t really think too much about lights when they are dreaming up their new 3D scene or game — after all, everybody knows the hard part is finding the perfect models and textures, and then you spend hours arranging them into the perfect scene. Once you’ve finished, all you need to do is add a light source and marvel at your new creation. Except, when you look at your new scene, you can’t help but feel like something is missing. This is because in our day-to-day lives we rarely appreciate how much lighting contributes to the tone, color, mood, and atmosphere of what we’re looking at. Lighting can transform your scene from a happy safe scene to a dark and ominous scene simply by adjusting the lighting.

This tutorial is going to cover the different types of lights in Three.js and explain how to technically use each one. This is not a tutorial on good artistic lighting techniques because, to be honest, I’m a horrible artist.

Getting Started

In this tutorial I’m going to begin with a simple HTML file similar to the one I used in my previous tutorial:

<!DOCTYPE HTML>
<html>
<head>
<title>WebGL/Three.js Light Tutorial</title>
<style>
body {
background-color:#cccccc;
margin: 0px;
overflow: hidden;
}
</style>
</head>
<body>
<script src="js/three.min.js"></script>
<script src="js/OrbitControls.js"></script>
<script src="js/three-light-tut.js"></script>
</body>
</html>

The HTML assumes that you have downloaded the minified three.js library and saved it in a folder named ‘js’. It also assumes that you have copied the OrbitControls.js file to the same location. The OrbitControls.js file can be found packaged with Three.js in the path ‘examples\js\controls’.

The OrbitControls will allow us to rotate the scene by left clicking and dragging, pan the scene by right clicking and dragging, and zooming by scrolling the mouse wheel. I’ve also created my application, three-light-tut.js, and saved it in the same ‘js’ folder. Below is the starting code for our application:

var camera;
var scene;
var renderer;
var controls;

init();
animate();

function init() {

// Create a scene
scene = new THREE.Scene();

// Add the camera
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 100, 250);

// Add scene elements
addSceneElements();

// Add lights
addLights();

// Create the WebGL Renderer
renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );

// Append the renderer to the body
document.body.appendChild( renderer.domElement );

// Add a resize event listener
window.addEventListener( 'resize', onWindowResize, false );

// Add the orbit controls
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.target = new THREE.Vector3(0, 100, 0);
}

function addLights() {
var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(100, 100, 50);
scene.add(dirLight);
}

function addSceneElements() {
// Create a cube used to build the floor and walls
var cube = new THREE.CubeGeometry( 200, 1, 200);

// create different materials
var floorMat = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/wood-floor.jpg') } );
var wallMat = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/bricks.jpg') } );
var redMat = new THREE.MeshPhongMaterial( { color: 0xff3300, specular: 0x555555, shininess: 30 } );
var purpleMat = new THREE.MeshPhongMaterial( { color: 0x6F6CC5, specular: 0x555555, shininess: 30 } );

// Floor
var floor = new THREE.Mesh(cube, floorMat );
scene.add( floor );

// Back wall
var backWall = new THREE.Mesh(cube, wallMat );
backWall.rotation.x = Math.PI/180 * 90;
backWall.position.set(0,100,-100);
scene.add( backWall );

// Left wall
var leftWall = new THREE.Mesh(cube, wallMat );
leftWall.rotation.x = Math.PI/180 * 90;
leftWall.rotation.z = Math.PI/180 * 90;
leftWall.position.set(-100,100,0);
scene.add( leftWall );

// Right wall
var rightWall = new THREE.Mesh(cube, wallMat );
rightWall.rotation.x = Math.PI/180 * 90;
rightWall.rotation.z = Math.PI/180 * 90;
rightWall.position.set(100,100,0);
scene.add( rightWall );

// Sphere
var sphere = new THREE.Mesh(new THREE.SphereGeometry(20, 70, 20), redMat);
sphere.position.set(-25, 100, -20);
scene.add(sphere);

// Knot thingy
var knot = new THREE.Mesh(new THREE.TorusKnotGeometry( 40, 3, 100, 16 ), purpleMat);
knot.position.set(0, 60, 30);
scene.add(knot);
}

function animate() {
renderer.render( scene, camera );
requestAnimationFrame( animate );
controls.update();
}

function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}

If you run the application now, you should see a really simple scene like the one below. It contains a wood floor, three brick walls, a red sphere, and a purple torus knot thingy.

Lighting1.jpg

From this point on we are mainly going to focus on the code in the ‘addLights()’ function. In fact, if you take a look at this function right now you can see that we define one light for our scene: a directional light.

Directional Lights

Directional lights are a common form of lighting when creating an outdoor scene, but can be used in any scene. A directional light represents a light similar to the sun. It’s a light that is very far away and shines in one direction. Just like the sun, because it’s so far away, all of the rays run parallel to each other. Also, because this light acts as if it’s infinitely far away, the position of the light does not matter, only the angle of the light…sort of. Here is the code for our light:

var dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(100, 100, 50);
scene.add(dirLight);

In the first line, we are creating the light with two properties: the first is the color of the light and the second is the intensity or brightness of the light. Feel free to adjust these and see how it affects the scene. The second line changes the position of the light, and finally, the third line adds it to our scene.

Why change the position of the light if the position does not matter? The reason is because in Three.js, the direction of the light is not determined by the rotation of the light, but rather by calculating the angle from its current position and the position of its target. Since I didn’t specify a target, the default target is located at the point 0,0,0. I could have achieved the same effect by not moving the light and specifying a target for our light and setting its position at -100, -100, -50.

Ambient Light

Let’s begin by adding the following two lines of code to the end of our addLights() function:
var ambLight = new THREE.AmbientLight(0x404040);
scene.add(ambLight);

Ambient lights only have one property: the color. The first line creates an ambient light with a soft gray color. If you refresh your browser you will see that by adding this light we have simply added a flat, gray shading to all of the items in our scene. In fact, it looks pretty bad. That’s because ambient lighting isn’t really lighting at all. It simply does what I stated before: adds a flat shade of some color to the entire scene. I once was given a tip on using ambient lighting:

  1. Remove it completely.
  2. Add all of your other lights.
  3. If there are parts of your scene that are not lit by any of your main lights, then add just enough ambient lighting to make those dark corners visible.
  4. If it still looks bad, then go back to step 1.

Here is what our scene looks like once we’ve added our ambient lighting:

Lighting2.jpg

Point Lights

Point lights are lights that act like light bulbs: you place them at a location and they will shine their light in all directions and light up anything that is in their range.

Let’s begin by removing all of the existing lights from our scene, so go ahead and replace the contents of the ‘addLights()’ function with the following:

var bluePoint = new THREE.PointLight(0x0033ff, 3, 150);
bluePoint.position.set( 70, 5, 70 );
scene.add(bluePoint);
scene.add(new THREE.PointLightHelper(bluePoint, 3));

var greenPoint = new THREE.PointLight(0x33ff00, 1, 150);
greenPoint.position.set( -70, 5, 70 );
scene.add(greenPoint);
scene.add(new THREE.PointLightHelper(greenPoint, 3));

Here we have created two point lights, one blue and one green. The constructor for the point light takes three properties: the color of the light, the intensity, and the distance at which the intensity falls to 0, or in other words, the range of the light. If you set the distance to be zero then the distance will be infinite. In the first three lines we create one of our point lights, set its position, and add it to our scene. The fourth line adds a PointLightHelper. This is not necessary for adding light to the scene, it’s a helper class that places a wireframed sphere at the location of our light to make it easier to visualize while creating the scene.

After removing our existing lights and adding the green and blue point lights, our scene looks like the following:

Lighting3.jpg

Spot Lights

Next we are going to add a spotlight to our scene. A spotlight is exactly what it sounds like: a light that shines from a given point in one direction with the light forming a cone shape.

Add the following code to the end of the ‘addLights()’ function:

var spotLight = new THREE.SpotLight(0xffffff, 1, 200, 20, 10);
spotLight.position.set( 0, 150, 0 );

var spotTarget = new THREE.Object3D();
spotTarget.position.set(0, 0, 0);
spotLight.target = spotTarget;

scene.add(spotLight);
scene.add(new THREE.PointLightHelper(spotLight, 1));

As you can see from the first line, we are specifying five properties in the constructor of the spotlight. Those properties are as follows: color, intensity, distance, angle, and exponent. We’ve covered color, intensity, and distance before. We have two new ones: angle and exponent. Angle is the angle at which the cone shape will take, or how wide the spot will be. The exponent is how fast the light will fall to 0 from its target direction. The higher the number, the duller the light.

In the second line, we set the position of our light. In the next three lines, we create an Object3D, set its position to be 0,0,0, then set that object to be the target of the spotlight. Just like the directional light, the direction the spotlight faces is not determined by the rotation of the light, but is calculated by finding the angle of the light and its target. In this case we set the target to be a generic Object3D class. Object3D is the parent of all 3D objects in Three.js, so we could have just as easily set the target of our spotlight to be one of our scene elements, like the sphere. That way, if we animated the sphere, the spotlight would automatically track the sphere. In fact, we will add a little animation to our spotlight in a bit, but for now if you refresh your browser you should see something like the following:

Lighting4.jpg

Hemisphere Lights

As you can see, our scene is still really dark, especially in the back corners of our room. We could add a low intensity directional light, or small amounts of ambient light to brighten these areas up a bit, but I’m going to add another kind of light called a hemisphere light. A hemisphere light is similar to ambient light in the regard that it has no location or direction.

Add the following to the end of our ‘addLights()’ function:

var hemLight = new THREE.HemisphereLight(0xffe5bb, 0xFFBF00, .1);
scene.add(hemLight);

As you can see, the hemisphere light takes three properties in its constructor. The first two are color, and the third is intensity. Why two colors? Hemisphere lighting is a way to add some ambient lighting, but with a little more realism. The first color represents the color of the light coming from above our model, like the sun or a ceiling light. The second color is the color of light coming from below our model, representing the color of the sun reflecting off the ground, or the color of a ceiling light reflecting off the floor. A gradient of those two colors is applied to the models in our scene. And just like the other lights, we can then specify the intensity of this light. It gives us a bit more control for fine-tuning compared with the ambient light.

Refresh your browser and you should see a scene like the one below:

Lighting5.jpg

The difference is subtle, but you should notice that you can now see the back corners of our scene.

Three.js provides a wide range of lighting options for your scene. Each light has a plethora of options, such as color, distance and intensity. And when used in combination with each other, the possibilities for lighting your scene are virtually limitless.

Before ending this tutorial, I want to add one final tweak to make our scene a little more dynamic. I’m going to animate the spotlight so that it kind of simulates a ceiling light hanging from a wire swinging back and forth.

To do this, we first need to declare our spotLight variable as global so we can get a reference to in the ‘animate()’ function. While we are at it, let’s declare another variable called counter.

Add the following to the top of our application:

var spotLight;
var counter = 0;

Change the following line in our ‘addLights()’ function:

var spotLight = new THREE.SpotLight(0xffffff, 1, 200, 20, 10);

to:

spotLight = new THREE.SpotLight(0xffffff, 1, 200, 20, 10);

Now add the following two lines to the ‘animate()’ function:

counter += .1;
spotLight.target.position.x = Math.sin(counter) * 100;

All we are doing here is updating the x position of the target the spotlight is pointing at. I calculate the position using a sin wave so we get a nice smooth swinging effect.

Refresh your browser and you should see our spotlight swinging back and forth.

Conclusion

Three.js provides a number of different lights you can use in your scene. Each light also provides a number of different options to control the effect of that light. When used in combination, you have endless options to enhance your 3D scene. There is no right or wrong way to use the lights, just play around with them to get the effect you are looking for. Good luck and happy lighting!

Be sure to check out Part 1 here: Getting Started with WebGL and Three.js and Part 2 here: WebGL and Three.js: Texture Mapping.

 

Header image courtesy of Unsplash.