Learning Three.js: Part 5: Controls

Requirements:

In Part 1, I covered some general terms and techniques used in Three.js, a three-dimensional rendering JavaScript library. Part 2 reviewed some of the geometry and material options in creating meshes. Part 3 demonstrated how to load and use textures (images) for those same meshes as a continuation of building from the first example code. Part 4 showed examples using the light source objects PointLight, SpotLight, and AmbientLight. In this fifth, final part, I cover different controls for Three.js.

Officially, there are no controls for Three.js. Inside of the library itself, there are no control schemes nor ways of moving objects via user input. However, inside the collection of examples are two different main ways to approach this problem: OrbitControls and PointerLockControls (FirstPersonControls).

OrbitControls

OrbitControls interprets mouse movement as rotational force. Through clicking and moving the mouse, all objects will rotate along the y-axis in that direction.

var camera, scene, renderer, geometry, material, mesh;
var texture, lightsource, controls;
function init() {
// Load a texture
texture = new THREE.TextureLoader().load( "checkered.png" );
// Create a scene
scene = new THREE.Scene();
// Create a geometry
// Create a box (cube) of 10 width, length, and height
geometry = new THREE.BoxGeometry( 10, 10, 10 );
// Create a MeshPhongMaterial with a loaded texture
material = new THREE.MeshPhongMaterial( { map: texture} );
// Combine the geometry and material into a mesh
mesh = new THREE.Mesh( geometry, material );
// Add the mesh to the scene
scene.add( mesh );
// Create an AmbientLight with the color white (0xffffff)
lightsource = new THREE.AmbientLight( 0xffffff);
// Add the light to the scene
scene.add( lightsource );
// Create a camera
// Set a Field of View (FOV) of 75 degrees
// Set an Apsect Ratio of the inner width divided by the inner height of the window
// Set the 'Near' distance at which the camera will start rendering scene objects to 2
// Set the 'Far' (draw distance) at which objects will not be rendered to 1000
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 2, 1000 );
// Move the camera 'out' by 30
camera.position.z = 30;
// Create a WebGL Renderer
renderer = new THREE.WebGLRenderer();
// Set the size of the renderer to the inner width and inner height of the window
renderer.setSize( window.innerWidth, window.innerHeight );
// Add in the created DOM element to the body of the document
document.body.appendChild( renderer.domElement );
// Add OrbitControls
controls = new THREE.OrbitControls( camera, renderer.domElement );
}

Using OrbitControls requires a simple, one-line change to the code from Part 4. We only need add a line for the controls and the code will handle input sources itself. However, while only the one line is needed in the example code, an entire, extra file should also be included in the webpage for full compatibility.

lojh7mt5bw

PointerLockControls (First Person Controls)

In order to react according to full mouse and keyboard input, the Pointer Lock functionality part of browsers can be set up, called, and used. However, this requires, like with OrbitControls, much more code and an additional file to work with Three.js.

Note: While there is an existing PointerLockControls object and file, I have written my own that combines different inputs into a single file.

var camera, scene, renderer;
var floorGeometry, floorMaterial, floorMesh, floorTexture;
var boxGeometry, boxMaterial, boxMesh, boxTexture;
var controls;
var prevTime = performance.now();
var velocity = new THREE.Vector3();
function init() {
// Load a texture
boxTexture = new THREE.TextureLoader().load( "checkered.png" );
floorTexture = new THREE.TextureLoader().load( "bw_checkered.png" );
// Wrap the floor texture
floorTexture.wrapS = THREE.RepeatWrapping;
// Repeat the texture by 16 times in both directions
floorTexture.repeat.set( 16, 16 );
// Create a camera
// Set a Field of View (FOV) of 75 degrees
// Set an Apsect Ratio of the inner width divided by the inner height of the window
// Set the 'Near' distance at which the camera will start rendering scene objects to 2
// Set the 'Far' (draw distance) at which objects will not be rendered to 1000
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
// Create a scene
scene = new THREE.Scene();
// Create a HemisphereLight source
var light = new THREE.HemisphereLight( 0xeeeeff, 0x777788, 0.75 );
light.position.set( 0.5, 1, 0.75 );
scene.add( light );
// Create First Person Controls
controls = new THREE.FirstPersonControls( camera );
scene.add( controls.getObject() );
// Create a PlaneGeometry for the floor
floorGeometry = new THREE.PlaneGeometry( 100, 100, 100, 100 );
// Roate the floor "down"
floorGeometry.rotateX( Math.PI / 2 );
// Create a floor material
floorMaterial = new THREE.MeshBasicMaterial( { map: floorTexture } );
floorMesh = new THREE.Mesh( floorGeometry, floorMaterial );
scene.add( floorMesh );
// Create a geometry
// Create a box (cube) of 10 width, length, and height
boxGeometry = new THREE.BoxGeometry( 10, 10, 10 );
// Create a MeshPhongMaterial with a loaded texture
boxMaterial = new THREE.MeshPhongMaterial( { map: boxTexture} );
// Combine the geometry and material into a mesh
boxMesh = new THREE.Mesh( boxGeometry, boxMaterial );
// Add the mesh to the scene
scene.add( boxMesh );
// Create a WebGL Renderer
renderer = new THREE.WebGLRenderer();
// Set the size of the renderer to the inner width and inner height of the window
renderer.setSize( window.innerWidth, window.innerHeight );
// Add in the created DOM element to the body of the document
document.body.appendChild( renderer.domElement );
}

view raw
part5_initExample.js
hosted with ❤ by GitHub

Building from the Part 4 code, and wanting to differentiate between the box and a ‘floor,’ the code now loads an additional texture, sets up a hemisphere light, plane geometry, and re-uses the same example code.

function animate() {
// Call the requestAnimationFrame function on the animate function
requestAnimationFrame( animate );
// Check the FirstPersonControls object and update velocity accordingly
playerControls();
// Render everything using the created renderer, scene, and camera
renderer.render( scene, camera );
}
function playerControls () {
// Are the controls enabled? (Does the browser have pointer lock?)
if ( controls.controlsEnabled ) {
// Save the current time
var time = performance.now();
// Create a delta value based on current time
var delta = ( time prevTime ) / 1000;
// Set the velocity.x and velocity.z using the calculated time delta
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
// As velocity.y is our "gravity," calculate delta
velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
if ( controls.moveForward ) {
velocity.z -= 400.0 * delta;
}
if ( controls.moveBackward ) {
velocity.z += 400.0 * delta;
}
if ( controls.moveLeft ) {
velocity.x -= 400.0 * delta;
}
if ( controls.moveRight ) {
velocity.x += 400.0 * delta;
}
// Update the position using the changed delta
controls.getObject().translateX( velocity.x * delta );
controls.getObject().translateY( velocity.y * delta );
controls.getObject().translateZ( velocity.z * delta );
// Prevent the camera/player from falling out of the 'world'
if ( controls.getObject().position.y < 10 ) {
velocity.y = 0;
controls.getObject().position.y = 10;
}
// Save the time for future delta calculations
prevTime = time;
}
}

While OrbitControls handled input for us, FirstPersonControls handles input but also needs an additional function, playerControls(), to update the velocity and “move” the camera as a result.

rsfrndap33

Part 5 FirstPersonControls Code (Full Example)

<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js&quot;></script>
<script src="FirstPersonControls.js"></script>
</head>
<body>
<script>
var camera, scene, renderer;
var floorGeometry, floorMaterial, floorMesh, floorTexture;
var boxGeometry, boxMaterial, boxMesh, boxTexture;
var controls;
var prevTime = performance.now();
var velocity = new THREE.Vector3();
function init() {
// Load a texture
boxTexture = new THREE.TextureLoader().load( "checkered.png" );
floorTexture = new THREE.TextureLoader().load( "bw_checkered.png" );
// Wrap the floor texture
floorTexture.wrapS = THREE.RepeatWrapping;
// Repeat the texture by 16 times in both directions
floorTexture.repeat.set( 16, 16 );
// Create a camera
// Set a Field of View (FOV) of 75 degrees
// Set an Apsect Ratio of the inner width divided by the inner height of the window
// Set the 'Near' distance at which the camera will start rendering scene objects to 2
// Set the 'Far' (draw distance) at which objects will not be rendered to 1000
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
// Create a scene
scene = new THREE.Scene();
// Create a HemisphereLight source
var light = new THREE.HemisphereLight( 0xeeeeff, 0x777788, 0.75 );
light.position.set( 0.5, 1, 0.75 );
scene.add( light );
// Create First Person Controls
controls = new THREE.FirstPersonControls( camera );
scene.add( controls.getObject() );
// Create a PlaneGeometry for the floor
floorGeometry = new THREE.PlaneGeometry( 100, 100, 100, 100 );
// Roate the floor "down"
floorGeometry.rotateX( Math.PI / 2 );
// Create a floor material
floorMaterial = new THREE.MeshBasicMaterial( { map: floorTexture } );
floorMesh = new THREE.Mesh( floorGeometry, floorMaterial );
scene.add( floorMesh );
// Create a geometry
// Create a box (cube) of 10 width, length, and height
boxGeometry = new THREE.BoxGeometry( 10, 10, 10 );
// Create a MeshPhongMaterial with a loaded texture
boxMaterial = new THREE.MeshPhongMaterial( { map: boxTexture} );
// Combine the geometry and material into a mesh
boxMesh = new THREE.Mesh( boxGeometry, boxMaterial );
// Add the mesh to the scene
scene.add( boxMesh );
// Create a WebGL Renderer
renderer = new THREE.WebGLRenderer();
// Set the size of the renderer to the inner width and inner height of the window
renderer.setSize( window.innerWidth, window.innerHeight );
// Add in the created DOM element to the body of the document
document.body.appendChild( renderer.domElement );
}
function animate() {
// Call the requestAnimationFrame function on the animate function
requestAnimationFrame( animate );
// Check the FirstPersonControls object and update velocity accordingly
playerControls();
// Render everything using the created renderer, scene, and camera
renderer.render( scene, camera );
}
function playerControls () {
// Are the controls enabled? (Does the browser have pointer lock?)
if ( controls.controlsEnabled ) {
// Save the current time
var time = performance.now();
// Create a delta value based on current time
var delta = ( time prevTime ) / 1000;
// Set the velocity.x and velocity.z using the calculated time delta
velocity.x -= velocity.x * 10.0 * delta;
velocity.z -= velocity.z * 10.0 * delta;
// As velocity.y is our "gravity," calculate delta
velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass
if ( controls.moveForward ) {
velocity.z -= 400.0 * delta;
}
if ( controls.moveBackward ) {
velocity.z += 400.0 * delta;
}
if ( controls.moveLeft ) {
velocity.x -= 400.0 * delta;
}
if ( controls.moveRight ) {
velocity.x += 400.0 * delta;
}
// Update the position using the changed delta
controls.getObject().translateX( velocity.x * delta );
controls.getObject().translateY( velocity.y * delta );
controls.getObject().translateZ( velocity.z * delta );
// Prevent the camera/player from falling out of the 'world'
if ( controls.getObject().position.y < 10 ) {
velocity.y = 0;
controls.getObject().position.y = 10;
}
// Save the time for future delta calculations
prevTime = time;
}
}
init();
animate();
</script>
</body>
</html>