Requirements:
- Some knowledge of JavaScript
- Optional: An editor like Atom, NetBeans, or Sublime Text
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).
- Part 1: Terms and Techniques
- Part 2: Geometries and Materials
- Part 3: Loading and Using Textures
- Part 4: Light Sources
- Part 5: Controls
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
Part 5 FirstPersonControls Code (Full Example)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r83/three.min.js"></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> |