In this step-by-step guide, we’re going to create a 3D version of the Treehouse logo using three.js, which is a 3D graphics framework built on top of WebGL. Click and drag your mouse to orbit the camera! You can also use your mousewheel to zoom in and out.
See the Demo Download the Code View on CodePen
Note: You’ll need the desktop version of either Chrome, Firefox, or Safari. See browser compatibility below.
3D graphics can be difficult, especially 3D in the browser. Frameworks like three.js make it a bit easier, but the official documentation is still under construction and there are a few quirks that can stop beginners from ever getting started. If you’re new to 3D, this guide will help you get started.
Even though three.js might look complex at first, it would actually take even more code to write the same thing in pure WebGL, mostly because we’d need to write a rendering engine. All the heavy lifting is done with three.js without sacrificing much flexibility.
Contents
Browser Compatibility
For this tutorial, you’ll need the desktop version of either Chrome, Firefox, or Safari. Unfortunately, WebGL doesn’t work on mobile browsers yet, and it won’t be available in Internet Explorer until version 11.
Also, if you’re using Safari, you need to enable WebGL first. Here’s how to enable WebGL in Safari:
- Open the Preferences menu.
- Click on the Advanced tab.
- Click the checkbox that says Show Develop menu in menu bar.
- Open the Develop menu from the menu bar and select Enable WebGL.
Here’s the caniuse.com matrix for WebGL compatibility. Hopefully support will pick up in the future, because this is a really cool technology!
Getting Started
Download three.js
Head over to http://threejs.org/ and click the “Download” link on the left side of your screen. Once the zip has finished downloading, open it up and go to the build folder. Inside, you’ll find a file called three.min.js and, if you’re following along, you should copy this file into your local development directory.
For this tutorial, you’ll also need a file called OrbitControls.js which is included in the three.js download. Here’s the file path:
threejs folder > examples > js > controls > OrbitControls.js
If you’d rather just grab the two files you need, they’re included with the example code for this tutorial.
Setup the Local Environment
JavaScript has a security feature called the same-origin policy, which means you cannot load externally hosted files inside of your JavaScript code. This can be slightly problematic, because three.js needs to load geometry, textures, and other files. In order to circumvent this issue, you’ll need a local http server so that your files come from the same origin. Simply opening the index.html file directly in the browser isn’t going to work.
Fortunately, there’s a three.js FAQ with an excellent guide on how to run three.js locally using either Python, Ruby, or adjusting your browser settings. It’s easier than it sounds, so if you’re scratching your head wondering why some files aren’t loading, check out the guide.
Create 3D Assets
I’ve already created a 3D version of the Treehouse logo that you’re welcome to use for learning purposes (you can find the mesh inside the code download), but if you’d like to create your own meshes, I recommend you use Blender. It’s a wonderful 3D modeling and rendering package that’s free, open source, and cross-platform. There’s also plenty of educational material out there (free and paid) to help you get started modeling. I used Blender for the first time and had my finished mesh in about an hour. There’s probably some optimizations I could have made (the mesh topology is actually a bit messy) but it works for this demo.
In order to export a mesh from Blender for use in three.js, you’ll need to open the utility folder in three.js and install the exporter. Here are instructions on how to export from Blender to three.js.
The HTML
Alright. Once you’ve got your files in place and your local environment setup, it’s time to start coding. Let’s get the HTML out of the way first, because that’s the easy part. You just need a basic template like this to get going. This also assumes your JavaScript is stored in a folder called js
, so check your file paths just in case.
index.html
<!doctype html>
Like I said, nothing special here. The magic happens between the script tags.
Using three.js to Create 3D Scenes
We could write our JavaScript externally, but since there aren’t any HTML elements inside of the body, I figured it would help make this example a bit more clear if inline script tags were used.
Global Variables and Functions
Inside our script tags, we want to set up some global variables and then call some functions, all of which will be defined later on:
// Set up the scene, camera, and renderer as global variables.
var scene, camera, renderer;
init();
animate();
Create the Scene
Three.js uses the concept of a scene to define an area where you can place things like geometry, lights, cameras, and so on. In the following code, we start writing our initialization function by creating a scene. Then, we store the width and height of the browser window in the variables WIDTH
and HEIGHT
. We’ll need them in more than once place later on, so it’s good to just grab them once and store them.
// Globals from the previous step go here...
// Sets up the scene.
function init() {
// Create the scene and set the scene size.
scene = new THREE.Scene();
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight;
// More code goes here next...
}
Create the Renderer
Next, we set up a three.js renderer. We could use the SVG or canvas renderers, but we want to use the WebGL renderer because it’s able to take advantage of the GPU, which makes it several orders of magnitude more performant. After creating the renderer, we append it to the DOM via the body element. This will make three.js create a canvas
inside the body element that will be used to render our scene.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
// Create a renderer and add it to the DOM.
renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(WIDTH, HEIGHT);
document.body.appendChild(renderer.domElement);
// More code goes here next...
}
Create a Camera
Once our scene and renderer are in place, we can create a camera. The PerspectiveCamera
takes a few parameters. They are:
- FOV – We’re using 45 degrees for our field of view.
- Apsect – We’re simply dividing the browser width and height to get an aspect ratio.
- Near – This is the distance at which the camera will start rendering scene objects.
- Far – Anything beyond this distance will not be rendered. Perhaps more commonly known as the draw distance.
After our camera is created, we set the position by using some simply XYZ coordinates. The default is 0,0,0
but I’ve set the Y value to 6 just to get some distance between our view and the mesh.
Finally, we need to add the camera to the scene.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
// Create a camera, zoom it out from the model a bit, and add it to the scene.
camera = new THREE.PerspectiveCamera(45, WIDTH / HEIGHT, 0.1, 20000);
camera.position.set(0,6,0);
scene.add(camera);
// More code goes here next...
}
Update the Viewport on Resize
This is all well and good, but what happens when the site visitor resizes the browser window? For that, we’ll need to add an event listener. When the browser is resized, a couple of things happen. First, we resample the new width and height of the browser and store it in a variable that’s scoped to the function. Then, we use those values to set the new size of our renderer as well as recalculate the aspect ratio of the camera. In addition, we need to call updateProjectionMatrix()
on the camera object so that our scene will actually update with the new parameters. This is computationally expensive in the context of real-time 3D rendering, but once the browser is resized, things click back to their normal frame rates.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
// Create an event listener that resizes the renderer with the browser window.
window.addEventListener('resize', function() {
var WIDTH = window.innerWidth,
HEIGHT = window.innerHeight;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
});
// More code goes here next...
}
Add Lighting
Now it’s time to start crafting our scene a bit. By calling the setClearColorHex
function on our WebGLRenderer
object, we’re able to set the background color of our scene to the Treehouse grey hex color with an opacity of 1.
Next, we’ll need a light in order to see our 3D objects, so we’ll add a PointLight
to the scene and set its position. There are several other kinds of lights you can add to a scene, so be sure to check out the linked documentation.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
// Set the background color of the scene.
renderer.setClearColorHex(0x333F47, 1);
// Create a light, set its position, and add it to the scene.
var light = new THREE.PointLight(0xffffff);
light.position.set(-100,200,100);
scene.add(light);
// More code goes here next...
}
Load Geometry
Our mesh has been exported from Blender using the three.js JSON exporter, so we need to use the JSONLoader
to get the geometry into the scene. A callback is used inside the loader to set the material on the mesh. In this case, we’re using a basic LambertMaterial
to set the mesh to Treehouse’s green color. For completeness, I should note here that the green you’re seeing in the final render isn’t quite the same as Treehouse’s logo green. That’s because the point light is skewing the brightness slightly, but we won’t worry about it for this demo.
Before leaving the callback function, we create a new mesh with our geometry and the material as parameters, then we add the mesh to the scene.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
// Load in the mesh and add it to the scene.
var loader = new THREE.JSONLoader();
loader.load( "models/treehouse_logo.js", function(geometry){
var material = new THREE.MeshLambertMaterial({color: 0x55B663});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
// More code goes here next...
}
Add Controls
The last thing in our initialization function is the orbit controls we included earlier. These aren’t totally necessary, but they do allow us to drag the mouse across the mesh and orbit around it. It also allows the mousewheel to be used to zoom in and out of the mesh.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
// Add OrbitControls so that we can pan around with the mouse.
controls = new THREE.OrbitControls(camera, renderer.domElement);
}
// More code goes here next...
Render the Scene
After our initialization function, we need to finish up with our animation function. It may not seem like anything is really “animated” here in the traditional sense, but we do need to redraw when the camera orbits around the mesh.
The requestAnimationFrame()
function uses a newer browser API that delegates redraws to the browser. This has some pretty cool benefits, but primarily it makes sure the browser isn’t drawing your animation unnecessarily if that tab isn’t currently selected. Paul Irish wrote an excellent blog post on requestAnimationFrame that explains this in more detail.
After that, we need to render our scene through the camera we added earlier and then update the orbit controls.
// Sets up the scene.
function init() {
// Code from previous steps goes here...
}
// Renders the scene and updates the render as needed.
function animate() {
// Read more about requestAnimationFrame at http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
requestAnimationFrame(animate);
// Render the scene.
renderer.render(scene, camera);
controls.update();
}
To see what the final result looks like, be sure to download the code. Try changing some of the parameters around and see what happens!
What’s Next?
I think three.js is an amazing project that unlocks the power of WebGL for people that aren’t 3D graphics gurus (like me). Browser support is still picking up, but I feel like some of the most practical applications of WebGL sit with product demos: Imagine exploring a new car in full 3D from the comfort of your browser.
However, such a low the barrier to entry makes it possible to create cool logos or make music videos without spending years writing renderers or other complex code. If you’re a little more dedicated, you could even make games and build worlds. It makes the browser a much more experiential place, which I think is pretty awesome. Who wants to make documents? I want to make places.