The client for the browser game I want to build is going to use ThreeJS to play. I don’t know much about ThreeJS so I’m documenting what I learn here to keep a record of how this was all done.

Environment Setup

The first thing we need is a webpage to build it all on! I run a local nginx server, but for this you really just need a folder with an index.html in it. I’m going to download a few resources into this folder so we have a consistent tree, since loading assets can be complicated enough without having to figure out the “implied” setup.

Create a folder and put an index.html in it. Mine is in ~/src/threejs-obj-space for lack of a better name. In this folder, download the assets we want to use.

Put the zip file from ThreeJS into a js/ folder and extract it (it should default to three.js-master/ ).

Put the two asset zips into an assets/ folder and extract them. For this first pass the only stuff we really need are the Models/ folder from the 3D Space Kit and the Backgrounds/ from the 2D Space Shooter Redux.

With minimal effort you should now have all the files you need in the right place (at least to line up with how I’m going to go through this walkthrough).

Initial Code

Now we need to build out the index.html to actually render our scene.

<html> <head> <style type="text/css"> html, body { margin: 0; padding: 0; background-color: #121212; } #game { width: 100%; height: 100%; display: block; } </style> </head> <body> <div id="game"></div> <script src="./js/three.js-master/build/three.js"></script> <script src="./js/three.js-master/examples/js/loaders/OBJLoader.js"></script> <script src="./js/three.js-master/examples/js/loaders/MTLLoader.js"></script> <script type="text/javascript"> /* Our Code will Go Here */ </script> </body> </html>

This is the framework for our browser game. We create a div container called #game so we can control how it’s styled and displayed, and we include the ThreeJS files we need to render and load the assets we have.

Next we need to add a few listeners so we can properly hook up with the relevant browser events. Inside the /* Our Code will Go Here */ block, add resize , DOMContentLoaded and requestAnimationFrame listeners. We will also need a few global variables we can populate so we can reference them directly.

var scene, camera, renderer; window.addEventListener("resize", onResize); document.addEventListener("DOMContentLoaded", onLoaded); let animate = () => { requestAnimationFrame(animate); update(); }; let update = () => { //This block will be called every frame }; let onResize = (event) => { //This block will be called when the user resizes the screen }; let onLoaded = (event) => { //This block is called once on page load animate(); };

Setting the Scene

ThreeJS needs a scene, a camera and a renderer to do anything. Inside the onLoaded function, above the animate() call, you can build these out.

var gameContainer = document.getElementById("game"); scene = new THREE.Scene(); scene.background = new THREE.Color(0x000000); camera = new THREE.PerspectiveCamera(66, gameContainer.clientWidth / gameContainer.clientHeight, 0.1, 1000); renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(gameContainer.clientWidth, gameContainer.clientHeight); gameContainer.appendChild(renderer.domElement);

This will get the div that we will contain the renderer in, and then set up a ThreeJS scene.

It creates a camera with a viewing angle of 66 degrees, a near of 0.1 and a far of 1000, with an aspect ratio to match the browser window.

It sets up a renderer to match the size of the div, and attaches the renderer DOM element to the container.

But before the renderer is actually hooked up, you need to add another line, this time inside the animate() function at the bottom.

renderer.render(scene, camera);

Tada! What you’ll see is an amazing… black screen. There’s nothing in our scene! But first we need to handle resizing, since if you adjust the browser window you’ll see that the renderer doesn’t change.

Inside the onResize() function, add the following.

let gameContainer = document.getElementById("game"); camera = new THREE.PerspectiveCamera(66, gameContainer.clientWidth / gameContainer.clientHeight, 0.1, 1000); renderer.setSize(gameContainer.clientWidth, gameContainer.clientHeight);

This will recreate (and replace) our perspective camera using the new browser window dimensions, and adjust the renderer to the new dimensions as well.

Setting the Scene

Now that we have the basics in place to display something, we need to add something to display. I started by loading in a tiled texture plane and an ambient light so I could see it. Inside the onLoaded function, add:

let ambientLight = new THREE.AmbientLight(0xffffff); ambientLight.intensity = 0.8; scene.add(ambientLight); let starfieldTexture = new THREE.TextureLoader().load('./assets/Backgrounds/blue.png'); starfieldTexture.wrapS = THREE.RepeatWrapping; starfieldTexture.wrapT = THREE.RepeatWrapping; starfieldTexture.repeat.set(20, 20); let geometry = new THREE.PlaneGeometry(500, 500, 256, 256); let material = new THREE.MeshLambertMaterial({map: starfieldTexture, color: 0xffffff}); let starfield = new THREE.Mesh(geometry, material); starfield.position.z = -10; scene.add(starfield);

Finally! The fruits of our labor:

… a dimly lit image. How exciting. It’s pretty neat though, considering now you could play around with the lighting intensity (anywhere from 0 – 1) or the background texture we’re loading (black.png and purple.png being other options available).

My favorite thing to do now is get a different perspective. Literally!

We have some duplication in the code; we create the camera twice in the load and resize. Instead, refactor out that creation to a separate function and call it in both places.

let buildCamera = () => { let gameContainer = document.getElementById("game"); camera = new THREE.PerspectiveCamera(66, gameContainer.clientWidth / gameContainer.clientHeight, 0.1, 1000); };

Now any changes you want to make to the camera can be done in the shared function. Let’s change the position and viewing angle so we get a different look at our starfield plane. Put these lines inside the buildCamera() function at the bottom.

camera.position.y = -25; camera.position.x = -25; camera.position.z = 50; camera.up = new THREE.Vector3(1, 1, 0); camera.lookAt(new THREE.Vector3(0, 0, 0);

This moves the camera upward (Z is up in ThreeJS) by 50 units, and left and down by -25 each. Then we tell the camera how to orient itself and look at the center of the plane. (0, 0, 0)

Look! Now we see the starfield at an angle. Thrilling! But wait, there’s more!

A Spacecraft Appears

This next part is complicated because it adds in a ton of things really fast. Since it’s all unified but complex, this is a perfect candidate for a function. First, add a function call inside the onLoaded() , below where we add the starfield to the scene.

... let starfield = new THREE.Mesh(geometry, material); starfield.position.z = -10; scene.add(starfield); addSpaceCraft(scene);

Now, make a function called addSpaceCraft and we’ll add in the Material and Object loader. Loaders have some additional functions that can provide extra information in case something goes wrong during this process, so we’ll write those to output their status so debugging is easier.

let onProgress = (data) => { console.log("onProgress", data); }; let onError = (error) => { console.error("onError", error); }; let addSpaceCraft = (scene) => { let materialLoader = new THREE.MTLLoader(); let objectLoader = new THREE.OBJLoader(); materialLoader.load('./assets/Models/spaceCraft1.mtl', (materials) => { materials.preload(); objectLoader.setMaterials(materials); objectLoader.load('./assets/Models/spaceCraft1.obj', (object) => { object.rotation.x = 90; scene.add(object); }, onProgress, onError); }, onProgress, onError); };

Now reload your page and take a look:

A spacecraft! Incredible! If you don’t see a ship at this point, check your browser console for some of those helpful log messages we put in. Things that might fail here are bad material data, bad object data, incorrect paths to the assets or even typos in the callback function that aren’t immediately apparent.

At this point, you have enough of the basics to be able to use the Kenny assets to build something out in ThreeJS.

But I’m not finished yet. Let’s add some better lighting and maybe a bit of a float effect on the spacecraft.

First, we need to save off the spacecraft so we can access it and manipulate it later. Create another few global variables at the top.

... var scene, camera, renderer; var spaceCraft; var oscillator = 0; ...

Near the line where we add the spacecraft object to the stage, save the object into our new spaceCraft global.

... object.rotation.x = 90; scene.add(object); spaceCraft = object; }, onProgress, onError); ...

For the floating effect, we’ll use a rudimentary oscillator in the update() function.

let update = () => { oscillator += 1; oscillator %= 360; if(spaceCraft) { spaceCraft.position.z = Math.sin(oscillator * Math.PI / 180); spaceCraft.rotation.z = Math.cos(oscillator * Math.PI / 180) / 8; } };

This will result in the spacecraft gently moving up and down while rocking back and forth. But the model itself still looks pretty flat, so let’s add in some lighting and shadows.

In the onLoaded() function, near where we added the ambient light, add a new function called addSpotLight .

... let ambientLight = new THREE.AmbientLight(0xffffff); ambientLight.intensity = 0.8; scene.add(ambientLight); addSpotLight(scene); ...

Inside this new function we will add a focused spot light so that the spacecraft material can be directionally illuminated. This will make the ship look a lot less flat and a lot more shiny.

let addSpotLight = (scene) => { spotLight = new THREE.SpotLight(0xffffff); spotLight.position.set(0, 0, 100); spotLight.angle = 0.18; spotLight.penumbra = 1; spotLight.intensity = 0.2; scene.add(spotLight); };

Now that is an awesome looking spaceship.

I hope this walkthrough can help others as much as it helped me to write it out. If you have any issues or comments, please let me know! I love hearing about game developers learning new things. Especially if they’re me!