ThreeJs Light Color and Action
LIGHTS! COLOR! ACTION!
Picking up where we left off in the last chapter, we’re going to add some life to our scene with animation, color, and lights.
Open up your code from the last chapter, or use this CodeSandbox which has the code from Chapter 1.1 already completed.
Code Organisation
Let’s take some time to structure our code better. So far we’ve just been adding lines one after another as we go, which was fine when our app was just a couple of lines long. But as our app grows in size, this will quickly become confusing.
We’ll wrap everything we’ve written so far in a function called
init()
, except for the line renderer.render( scene, camera )
which will go inside an animate()
function. Once we’ve done this, our code will look like this:// these need to be accessed inside more than one function so we'll declare them first
let container;
let camera;
let renderer;
let scene;
let mesh;
function init() {
// Get a reference to the container element that will hold our scene
container = document.querySelector( '#scene-container' );
// create a Scene
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x8FBCD4 );
// set up the options for a perspective camera
const fov = 35; // fov = Field Of View
const aspect = container.clientWidth / container.clientHeight;
const near = 0.1;
const far = 100;
camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
// every object is initially created at ( 0, 0, 0 )
// we'll move the camera back a bit so that we can view the scene
camera.position.set( 0, 0, 10 );
// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );
// create a default (white) Basic material
const material = new THREE.MeshBasicMaterial();
// create a Mesh containing the geometry and material
mesh = new THREE.Mesh( geometry, material );
// add the mesh to the scene object
scene.add( mesh );
// create a WebGLRenderer and set its width and height
renderer = new THREE.WebGLRenderer();
renderer.setSize( container.clientWidth, container.clientHeight );
renderer.setPixelRatio( window.devicePixelRatio );
// add the automatically created <canvas> element to the page
container.appendChild( renderer.domElement );
}
function animate() {
// render, or 'create a still image', of the scene
renderer.render( scene, camera );
}
// call the init function to set everything up
init();
// then call the animate function to render the scene
animate();
Code organization is extremely important and will make any piece of software much easier for both you and other people to understand and maintain. We’ll be further improving our code structure as we go, but for now, it looks pretty good. Let’s move on.
Introducing the Animation Loop
You may have noticed that, in our reorganized code, the
animate()
function is badly named - as the comment in the code points out, renderer.render( scene, camera )
renders a still image of the scene
from the point of view of the camera
- and it seems a little unfair to call a single image an animation.
Let’s fix that. We want to call
animate()
once before each frame so that we can perform any updates to our scene and then render a new frame.
To do so, we’ll use a method that is built into every modern browser, called
requestAnimationFrame()
, or window.requestAnimationFrame
to give it it’s full title:
Recursively Calling animate()
Using requestAnimationFrame()
The updated, recursive animate function
function animate() {
// call animate recursively
requestAnimationFrame( animate );
// render, or 'create a still image', of the scene
// this will create one still image / frame each time the animate
// function calls itself
renderer.render( scene, camera );
}
Using
requestAnimationFrame
is pretty straightforward - the trick is to call it recursively. A recursive function is simply a function that calls itself repeatedly - refer back to our brief JavaScript Tutorial in the intro if this concept is unfamiliar to you.
So, we’ll call our
animate
function within our animate
function, using requestAnimationFrame
to handle the timing. Once we’ve done so, our app should be updating at a smooth 60 frames per second.
Well, except of course, that our scene doesn’t actually look any different. It may be updating at up to 60 frames per second, but nothing is moving so we can’t see that this happening.
We’ll add some movement soon, but first, let’s add some lights and a splash of color.
Color in three.js
We saw the
Color
object in the last chapter when we set the background to a light blue color using scene.background = new THREE.Color( 'skyblue' )
.
As we mentioned then, we can use any of the CSS color names here. However, there are only 140 of them. This might seem like a lot, but it’s a long way from the 16,777,216 colors that a standard computer monitor can display. How do we go about specifying the rest?
Actually, there are a number of ways, as you’ll see if you take a look at the
Color
docs page. We’ll mainly be sticking with one in this book though since it’s the same method most often used in CSS: Hexadecimal Triplets, commonly known as Hex Colors.Setting a Material’s Color
// create a geometry
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );
// create a purple Basic material
const material = new THREE.MeshBasicMaterial( { color: 0x800080 } );
// create a Mesh containing the geometry and material
mesh = new THREE.Mesh( geometry, material );
Let’s use the hex code
0x800080
(purple) to set our material’s color. This time we don’t need to use the Color
constructor, we can just pass in color: 0x800080
as a parameter to the Material
constructor and it will call the Color
constructor automatically for us.
The
color
instance that gets created is stored in material.color
, which we can use if we want to update the material’s color at any time.
The important thing to remember here is that
material.color
is an instance of Color
- we’ll need to use the Color.set( newColor )
method if we want to update it. For example, to change the material’s color to red we can do this: material.color.set( '0xff0000' )
.Switch to a Higher Quality Material
Change your old, boring Basic material
const material = new THREE.MeshBasicMaterial( { color: 0x800080 } );
…into a fancy new physically-correct Standard material
// create a purple Standard material
const material = new THREE.MeshStandardMaterial( { color: 0x800080 } );
We’ll switch our
MeshBasicMaterial
for a higher quality (but lower performance) MeshStandardMaterial
.
The
MeshBasicMaterial
material doesn’t react to lights at all, whereas the MeshStandardMaterial
material is a physically correct material, meaning that it reacts to light in the same way an object in the real world does, or at least with a fairly good approximation.
The Basic and Standard materials lie at the two extremes of the performance/quality spectrum in three.js, and there are plenty of other material types available in between, as we’ll see in Section 4: Materials And Textures.
Let’s switch over our material …And our scene has gone completely black. Great.
Add Light
Add a directional light
scene.add( mesh );
// Create a directional light
const light = new THREE.DirectionalLight( 0xffffff, 5.0 );
// move the light back and up a bit
light.position.set( 10, 10, 10 );
// remember to add the light to the scene
scene.add( light );
// create a WebGLRenderer and set its width and height
So we need a light then? No problem. As you might expect by now, there are lots of options, as we’ll see in Section 5: Lights And Shadows.
For now, we’ll choose a
DirectionalLight
. Directional lights mimic light from a distant source such as the sun, and shine from a position, towards a target. By default, this target is at .
We’ve also moved the light back and up a bit by setting its position to , so the light is now shining from towards .
Once you’ve added the light, your mesh should be visible again. Not only that, but it’s now a nice purple color, set against an equally nice light blue background. Things are looking sweet, although we’re facing our cube head on so it still looks like a square.
Add Movement
Add some action to the scene by rotating the box at the start of each frame, in the animate function
function animate() {
// call animate recursively
requestAnimationFrame( animate );
// increase the mesh's rotation each frame
mesh.rotation.z += 0.01;
mesh.rotation.x += 0.01;
mesh.rotation.y += 0.01;
// render, or 'create a still image', of the scene
// this will create one still image / frame each time the animate
// function calls itself
renderer.render( scene, camera );
}
As noted above, our scene should now be animating nicely at somewhere close to 60 frames per second, but we have no way of telling this since nothing is moving. We want to keep things simple for now, so we’ll just add a small amount of rotation to the cube each frame to give it a random looking tumble.
We’ll add to the , , and components of the cube’s rotation each frame.
The above changes will give our cube a nice random looking tumble:
Antialiasing
Add antialiasing to the WebGLRenderer
scene.add( light );
// create a WebGLRenderer and set its width and height
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( container.clientWidth, container.clientHeight );
We’ll finish up by making one final change, which will improve the look of our scene a lot. We’ll pass in a new parameter to the
WebGLRenderer
, setting the antialias
property to true
. Without going into any great detail here, this makes jagged lines look much smoother.
Just as with the material constructor, we need to pass a spec object with named parameters into the
WebGLRenderer
constructor. However, unlike the material.color
, we can’t change this setting after the renderer has been created. If we want to change it we’ll need to create an entirely new renderer.
When we turn this setting on, anti-aliasing is performed using the built-in WebGL method, which is currently multisample anti-aliasing. Depending on your browser and graphics card, there is a chance that this will be unavailable or disabled, although on modern hardware that is unlikely.
The Effects of Lighting
With the above changes, our app finally looks 3D! Well done. Try changing back and forth between
MeshStandardMaterial
(left) and MeshBasicMaterial
(right) to see how the lights make an object look 3D:Final Result
We’ve already covered quite a bit of theory and hopefully, you are beginning to get a feel for how a basic three.js app is put together. In the next chapter, we’ll further improve the structure of our code and take a look at how to make our app resize automatically when the browser window size changes, for example when a user changes their phone from portrait to landscape or drags the window on their computer.
Comments
Post a Comment