WebGL with three.js
There is a spinning green cube somewhere on this page. It is not a video, it is not a gif – it is being rendered in real-time by your browser’s GPU. That is what WebGL can do, and in this post I will show you how to get there in about 30 lines of JavaScript with a library called Three.js.
Everything is moving away from the compiled native app and over to the web browser. This has the advantage of giving users (those not using IE) the same experience no matter what platform the webapp is running on. One relatively new api that has come out of this shift is WebGL.
WebGL is a way for web developers to directly access the OpenGL apis. WebGL has shown some remarkable adoption over the past few years and I have to think this is due to it being based on the very stable open apis from OpenGL. If you have used OpenGL (or any 3D programming) there is a lot to do before anything can be rendered to the screen. I am not talking about art assets either, I am talking about templated boilerplate code that makes this stuff very tedious to start with. Luckily there are lots of Javascript people writing libraries so I don’t have to rewrite the same boiler-plate code over and over again.
Enter Three.js
Three.js is a boiler-plate framework for building WebGL applications. It takes care of much of the work that normally comes from building 3D apps and you can get up and running with a few lines of code.
The code below is used to draw the spinning cube you see above.
<script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r67/three.min.js"></script>
<script>
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 640/480, 1, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(640, 480);
renderer.setClearColor(0x000000, 0);
jQuery("#content article .field-name-body").append(renderer.domElement);
jQuery("#content article .field-name-body canvas").css( {
"margin": "1em auto",
"display": "block"
});
var geometry = new THREE.BoxGeometry(1,1,1);
var material = new THREE.MeshLambertMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
var fog = THREE.Fog(0x000000, 1, 5);
var ambient = new THREE.AmbientLight( 0x101030 );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
directionalLight.position.set( 0, 1, 0 );
scene.add(cube);
scene.fog = fog;
scene.add(ambient);
scene.add(directionalLight);
camera.position.z = 2;
var render = function () {
requestAnimationFrame(render);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
render();
</script>
Lets dive into the code.
<script src="//cdnjs.cloudflare.com/ajax/libs/three.js/r67/three.min.js"></script>
This first line just loads the current release of three.js from a cdn.
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, 640/480, 1, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(640, 480);
Initialize!!
First thing we do to initialize the scene, camera, and renderer.
- scene
- The scene is an object that controls the 3D space that is rendered.
- camera
- The camera controls how the scene is rendered. Options include:
- OrthographicCamera (no perspective, isometric view)
- PerspectiveCamera (camera with perspective projection)
- The camera controls how the scene is rendered. Options include:
- renderer
- The renderer controls what is drawn on the screen and how. CanvasRenderer should be used in the case where WebGLRenderer is not supported. CanvasRenderer draws the scene as best as it can without GPU acceleration.
jQuery("#content article .field-name-body").append(renderer.domElement);
jQuery("#content article .field-name-body canvas").css( {
"margin": "1em auto",
"display": "block"
});
The renderer will create a canvas DOM object to use to draw the scene to the browser window. However, you still have to add the canvas to the page or alternatively you can pass the canvas element to the renderer constructor. In this case we are using jQuery to prepend the canvas element into the body field of this article. We are also assigning some style to the canvas element.
var geometry = new THREE.BoxGeometry(1,1,1);
var material = new THREE.MeshLambertMaterial({color: 0x00ff00});
var cube = new THREE.Mesh(geometry, material);
var fog = THREE.Fog(0x000000, 1, 5);
var ambient = new THREE.AmbientLight( 0x101030 );
var directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
directionalLight.position.set( 0, 1, 0 );
geometry, material, and cube
Geometry and materials are used to define a renderable object in 3D space. The geometry defines the actual mesh of the object. The material defines how the object is seen. These two things are brought together to define the cube.
Geometry is fairly simple, the material is where things get interesting. Polygon count doesn’t mean nearly as much as it used to; far more impressive things are made with shaders and materials are what define how the shader interacts with the object (in this case the cube). For this simple demo I used the MeshLambertMaterial. All that means is that the object will interact with lights. There are many different things that a talented artist can do with lights and shaders on a simple two poly plane. That however, is far beyond the scope of this post. For more on the power of shaders watch this talk from JSConfUS on Shaders.
fog, ambient, and directionalLight
These are all made to make the visual more appealing. Fog creates a gradually increasing shadow (which cannot even be noticed on my cube), ambient is the natural ambient lighting, and directionalLight is light that comes in from a set point. Light without direction has no shadows so this is important to the scene.
scene.add(cube);
scene.fog = fog;
scene.add(ambient);
scene.add(directionalLight);
camera.position.z = 2;
Add it all up
Once everything is defined then we simply add everything to the scene. Fog is a property of the scene so it isn’t added in the same way. Once everything is complete we set the camera distance.
var render = function () {
requestAnimationFrame(render);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
render();
One more thing
One more thing is required to make the scene update. If the scene had no movement then there would be no point in making it use realtime hardware processing. By creating the render function and then calling that function with requestAnimationFrame and renderer.render we are creating the game loop and allowing updates to occur. This is why the cube is spinning.
Where to go from here
That is all it takes to get a 3D scene running in a browser. A scene, a camera, a renderer, some geometry with a material, a light or two, and a render loop. Three.js handles the boilerplate so you can focus on what you want to build. From here, try swapping in different geometries, adding textures, or loading a real model. The Three.js documentation and examples page are the best next steps.
