Structuring large three.js applications with amd-three.js

Recently I’ve been building something using Three.js, and started out by basing my code on one of the many excellent examples. Soon enough I was in a world of bliss, full of lights and shaders and all the other niceness gives you.

Unfortunately, my JavaScript code quickly started to get a bit bloated and hard to navigate, and I decided that it was to time to split up my code into separate files and modules to make it more manageable. I couldn’t find much guidance on how others had done this in the past, so I decided to roll my own solution using Require.js. The result is amd-three.js, with a live example here.

amd-three.js is meant as a starting point for more involved projects involving three.js, where having all the code in one file can quickly get unwieldy. It is also useful for prototyping, as a lot of the boilerplate is moved out of your way.

Require.js in 29 seconds

If you haven’t come across Require.js, it is a JavaScript module loader. With it you can split your code into modules, passing in dependencies and having Require.js figure out how to link stuff up for you.

A module, container.js might look like this:

define( ["three"], function( THREE ) {
  return document.getElementById( 'threejs-container' );
} );

…while a module that includes container.js like this:

define( ["three", "container"], function( THREE, container ) {
  // do something with container
} );

That should give you enough of an idea of what Require.js does, for more detail see http://requirejs.org

amd-three.js structure

Following Require.js convention, the JavaScript file structure is like so:

/js/
  |--require.js
  |--main.js
  /app/
    |--app.js
    |--camera.js
    |-- other app files
  /lib/
    |--three.js
    |--three.min.js
    |-- other three.js components

main.js

This is the entry point into the code, where we configure require.js and start the app.

/app/app.js

Here we have the meat of the app, with two methods init() and animate(), which main.js will call for us to get the app running.

/lib/

The lib directory is for storing the three.js library and extensions. More on this below.

app.js

When you first check out the amd-three.js source, the first place you’ll likely want to go to make modifications is /app/app.js. Here’s how it might look:

define( ["three", "camera", "geometry", "light", "material", "renderer", "scene"],
function( three, camera, geometry, light, material, renderer, scene ) {
  var app = {
    mesh: new THREE.Mesh( geometry["cube"], material["grass"] ),
    init: function() {
      scene.add( app.mesh );
      light.target = app.mesh;
    },
    animate: function() {
      requestAnimationFrame( app.animate );
      app.mesh.rotation.x += 0.005;
      renderer.render( scene, camera );
    }
  }
  return app;
} );

As you can see, quite a few dependencies are being passed in. These are all located in files with corresponding names, e.g. the source to the camera object is in app/camera.js.

The above code first creates a mesh (app.mesh) from a geometry (geometry["cube"]) and a material (material["grass"]). The init method then adds it to the scene and points the light at it. Finally, the animate method rotates our mesh and tells our renderer to render.

Singletons

The modules that we depend on in app.js also have dependencies, and it is often the case that these dependencies are shared with app.js. For example, light.js needs to know about the scene so that it can add itself to it:

define( ["three", "scene"], function( THREE, scene ) {
  var light = new THREE.DirectionalLight( 0xff3bff );
  light.position.set( 0, 0, 300 );
  scene.add( light );
  return light;
} );

An important thing to realize is that the scene object being passed in here is the same scene object that app.js receives – as Require.js only initializes the modules once. As such, we can be sure that the light will shine on the same scene that we later add the mesh to.

Bundling up three.js

Unfortunately, three.js currently doesn’t come as an AMD module and as such we need to build a shim around it, so we can include it in other modules. For details on how this is done, see the Require.js documentation. One thing to note is that the /lib/three.js file is used to bring together all of the Three.js components, so that we don’t have to inject them all separately every time we want to use Three.js.

Also, the shim does not remove the THREE object from the global namespace, so technically we don’t need to pass it in as an explicit dependency into our modules. However, it good for future compatibility to do so, so that our code continues to work when an AMD-compliant Three.js module does appear.

…well?

I’d love to hear from others who have created larger projects with Three.js about their experiences, and views on this approach. Be sure to check out the code on github as well as the live example