
Interactive experiences have taken a bit of a bashing in the last few years; sites that featured heavy content were criticised for being inaccessible and all style with no substance.
Since then, there has been a heavy swing back towards web standards and, while WebGL isn’t yet a fully fledged web standard, it has good browser support coverage, with iOS being the only major browser without support for it. We are going to use WebGL to create an incredibly fast-running game, which is easily achieved since WebGL offloads the graphics to the GPU. Now with sites like the Gravity movie site, interactive experiences are becoming more balanced, since they can make use of WebGL to create a rich interactive environment.
This game is a platform game that uses the cursor keys to move left and right, while the up key controls jumping. The purpose of the game is to get the key and get out through the door while the water rises up, creating pressure. In this part of the tutorial we will be setting the scene up and doing the preliminary work so that next issue we can put it all together into a working game. The game will feature two levels, but it can easily have many more added using the same technique.
Start the project
From the resource CD, copy the tutorial folder to your web server, your local server or, if you are using Brackets, to anywhere on your hard drive. We have created some of the stylesheets and some of the JavaScript, which is extensively commented. Open ‘start.html’ and add the following just after the body tag.
001 <div id=”blank”><div id=”loading”><p> <img src=”img/logo.png”></p> <h3 class=”message”>Loading Please Wait </h3></div> </div> 002 <div id=”ThreeJS”></div>
Set up the variables
The previous HTML sets up a loading screen that sits in place until all the models are loaded – later we’ll use jQuery to change the message to play the game. Now find the opening ‘script’ tag and add this before the ‘loadModels’ function. This sets up variables that we will use in the game.
001 var container, scene, camera , renderer, composer; 002 var clock = new THREE.Clock(); 003 var direction = “”, openDoor = false, level = 1, inPlay=false; 004 var player, water, door, key, platforms , room, furniture, enemy, ray, rayUp; 005 var collidableMeshList = [];
Add more variables
Directly under the last code we add some more variables. Here we set whether the left or right key is being pressed and set the velocity of our player, along with settings for gravity, the speed that the water will rise in the game and the speed of the enemy. Other settings include the shadows and render effects.
001 var lfHeld = false, rtHeld = false; 002 var vy = 0, vx = 0, loaded = 0, gravity = 0.3, rise = 0.2, eSpeed =0.6; 003 var jumping = false, inAir = true, falling = false; 004 var SHADOW_MAP_WIDTH = 2048, SHADOW_MAP_ HEIGHT = 2048; 005 var renderPass, copyPass, effectFocus, composer, hblur, vblur; 006
jQuery document ready
When all the content on the page has loaded we use a jQuery document ready function and call the ‘init’ function, which sets up the game world for us. ThreeJS uses AJAX to load the models, so we still need our own management structure to check the models have loaded.
001 $(function() {
002 init();
003 });
Set up the scene
The first stage of working in 3D is to create a new scene. Here we do so and find out the width and height of the browser window. We also set up how near and far from the camera we will render content. A camera is set up with those settings and added into our scene.
001 function init() {
002 scene = new THREE.Scene();
003 var SCREEN_WIDTH = window.innerWidth,
SCREEN_HEIGHT = window.innerHeight;
004 var VIEW_ANGLE = 45, ASPECT = SCREEN_
WIDTH / SCREEN_HEIGHT, NEAR = 0.1,
FAR = 20000;
005 camera = new THREE.PerspectiveCamera
( VIEW_ANGLE, ASPECT, NEAR, FAR);
006 scene.add(camera);
Look at me!
Now we add the code that positions the camera slightly up on the y axis and a little further away from the centre of our world on our z axis. The camera is then told to look at the centre of the world. We detect if the user has WebGL available and we render using that, otherwise it’s the canvas renderer.
001 camera.position.set(0,150,400);
002 camera.lookAt(scene.position);
003 if ( Detector.webgl )
004 renderer = new THREE.WebGLRenderer
( {antialias:true} );
005 else
006 renderer = new THREE.CanvasRenderer();
007
Set the renderer up
The renderer is now set to match the screen width and height so that it fills the browser. The auto clear is set to false because of the post-processing effects that will occur later. We also add soft shadows to the renderer so that we have some depth and shadows in the scene.
001 renderer.setSize(SCREEN_WIDTH, SCREEN_ HEIGHT); 002 renderer.autoClear = false; 003 renderer.shadowMapEnabled = true; 004 renderer.shadowMapSoft = true; 005
Post-process the scene
ThreeJS comes with a plug-in architecture for various post-processing effects, we are going to take advantage of several of these to give the scene an interesting look. We have to set up a render pass and then we process that to add a bloom pass, which shows up highlights, a film grain pass and a vignette effect on the edges of the screen.
001 var renderModel = new THREE.RenderPass ( scene, camera ); 002 var effectBloom = new THREE.BloomPass ( .8 ); 003 var effectFilm = new THREE.FilmPass ( .3, .3, 1024,false ); 004 var shaderVignette = THREE. VignetteShader; 005 var effectVignette = new THREE. ShaderPass( shaderVignette );
Vignette settings
The vignette needs some settings to work properly, so we add an offset to it and set the vignette to have dark edges on the scene. This has the effect of drawing the player’s attention to the action in the centre of the screen. The camera will follow the player so that this is where the action will always be.
001 effectVignette.uniforms[ “offset” ] .value = 0.95; 002 effectVignette.uniforms [ “darkness” ].value = 1.8; 003 effectFilm.renderToScreen = true; 004
Add the passes
The next part of getting the post-processing effects to work is to set up a composer to render the passes. Each pass is then added to this so that the desired effect is achieved. This is a good time to save your work, so if you haven’t done already, save your progress.
001 composer = new THREE.EffectComposer ( renderer ); 002 composer.addPass( renderModel ); 003 composer.addPass(effectVignette); 004 composer.addPass( effectBloom ); 005 composer.addPass( effectFilm );
Add to webpage
As the rendering is all set up we need to add this to an element in the DOM. Here we select a <div> with the ID ‘ThreeJS’ and add our renderer to this element. To see any element in a 3D scene we also need lights. We are adding a spotlight that will point towards the centre of the world from the front, right and slightly above the scene.
001 container = document.getElementById ( ‘ThreeJS’ ); 002 container.appendChild( renderer. domElement ); 003 var light = new THREE.SpotLight ( 0xd6e2ff, 1, 0, Math.PI, 1 ); 004 light.position.set( 600, 1000, 1000 ); 005 light.target.position.set( 0, 0, 0 );
Set the shadow
The light is a very light blue and is set to be the light that will cast the shadow in the scene. It’s possible to have multiple lights that cast shadows, but even though there are going to be multiple lights in our scene, we will only have one that casts a shadow. It’s set up similar to the camera with a near and far field and a field of view.
001 light.castShadow = true; 002 light.shadowCameraNear = 200; 003 light.shadowCameraFar = 1800; 004 light.shadowCameraFov = 45; 005
A little bit shady
There are a number of settings that we need for shadows and these control the darkness of the shadows, we want them reasonably dark so that there is some good contrast in the scene, but not too dark that it ends up looking odd. The shadows are set to the height and width that we set earlier on in step 3.
001 light.shadowBias = 0.0005; 002 light.shadowDarkness = .55; 003 light.shadowMapWidth = SHADOW_MAP_WIDTH; 004 light.shadowMapHeight = SHADOW_MAP_ HEIGHT; 005 light.shadowMapSoft = true;
Another light
We add the first light to the scene and then we add another light to the scene. This light is a slate blue in colour and it is set to a very low brightness while being made to point downwards. This is acting as a specular light to pick up highlights and is added into the scene as well.
001 scene.add( light ); 002 var specLight = new THREE.PointLight ( 0x3a6f90, .1, 0, Math.PI, 1 ); 003 scene.add(specLight); 004
Raycasting for collisions
In 3D it is possible to fire an invisible ray from any direction of an object; this ray can bring a list of all the objects in that direction and how close they are. This is commonly used to check the distance for collisions. In the following code snippet we are sending one down from the player for landing on platforms and one upwards for hitting platforms when jumping.
001 ray = new THREE.Raycaster(); 002 ray.ray.direction.set( 0, -1, 0 ); 003 rayUp = new THREE.Raycaster(); 004 rayUp.ray.direction.set( 0, 1, 0 );
Waters rising
In the game we will have water rising in the room so that the player must constantly seek out higher ground. Here we create a plane that will look like water; we make this an ‘additive’ blue colour (which makes everything appear a light blue below it) and adjust the transparency to create the perfect water effect.
001 var waterGeometry = new THREE.
PlaneGeometry( 1000, 800, 1, 1, 1 );
002 var waterMaterial = new THREE.
MeshBasicMaterial( {color: 0x0042ff,
transparent: true, opacity: 0.95,
blending: THREE.AdditiveBlending} );
Delayed animation
The water model is made out of the geometry and material. This is rotated 90 degrees on the x axis so that it is flat in the scene. We also name this water and when we do collision detection in the game, we can find out if the model that the player lands on is ‘water’ and therefore end the play. Because the player will be colliding with the water, it gets added to an array.
001 water = new THREE.Mesh(waterGeometry, waterMaterial); 002 water.rotation.x = -Math.PI /2; 003 water.name=”Water”; 004 scene.add(water); 005 collidableMeshList.push(water); 006
The doors of perception
The door in every scene will be opened, so we need a separate model for this – a cube works well. Here we create the cube geometry and use the same material as the water. The cube is then positioned and added to the scene in the right place, ready to be opened when the player has collected the key.
001 var cubeGeometry = new THREE. CubeGeometry(60,60,10,1,1,1); 002 door = new THREE.Mesh(cubeGeometry, waterMaterial); 003 door.position.set(0, 485, -60); 004 door.geometry 005 scene.add( door );
User input
The next section of code checks for user input and whether the key is down or not. Using a ‘switch’ statement that is very similar to an ‘if’ statement, we check if the key is number 37, which is the ‘left’ key. If it is, the switch is broken and the code stops running in the function.
001 document.addEventListener(‘keydown’,
function(e){
002 switch(e.keyCode){
003 case 37: //left
004 direction = “left”;
005 break;
Up and right
The remaining section of the switch statement checks for the ‘up’ key being pressed and if it is, then the jump function is called, moving the player upwards. If the final key is pressed, which is the ‘right’ key, the direction is set to ‘right’. In the game the direction will control the movement on each frame.
001 case 38: //up 002 jump(); 003 break; 004 case 39: //right 005 direction = “right”; 006 break; 007 }; 008 }, false);
Release the keyboard
It is important to find out if the player has released a key as this will determine the responsiveness of the movement. Here we check if the left key is released and set the direction to equal nothing, so movement is ceased.
001 document.addEventListener(‘keyup’,
function(e){
002 switch(e.keyCode){
003 case 37: //left
004 direction = “”;
005 break;
And finally…
We check that the right key is released and likewise set the direction to equal nothing. Save this now and test in the browser. Because the render is not continuously being called the screen doesn’t look right yet, but you can get a sense of the visual style of the game at this early stage.
001 case 39: //right 002 direction = “”; 003 break; 004 }; 005 }, false); 006 loadModels();
