11 minutes read

An introduction to Three.js

On my last post I added a little animation to mark the occasion. I though I do something different so I decided to try WebGL and after meticulous googling1 I decided to try out Three.js, which turned out quite alright as getting it to work was a breeze and the only issues I had was with the animation itself and never with any of the technical bits.

WebGL itself seems pretty well supported, at least in all modern browsers - including mobile - and because things get drawn on a <canvas> element, embedding stuff with the rest of your web app / site is a mater of CSS’ing your way around it.

Before going into a short how-to, you can get the whole source code for on the Github Rosetta P67 Webgl page, if you do fix my math - I’m sure it’s easy :) - open a pull-request and happy days. The code samples are all CoffeeScript and run in the context of a bigger class, but hopefully they are informative enough on their own or as guidelines when reviewing the actual code.

For reference, all the examples will be based on the this code

The Basics of Three.js

So, in order for us to have something to look at we need to set up a few things, namely:

  1. A Scene - this will hold our objects, our lights and our camera.
  2. A Camera - this is our point of view, we’ll point it to the objects we want to see and control field of view, aspect ratio, etc.
  3. The objects and materials themselves - in our case, one comet with texture and a Rosetta spacecraft with a simple color.
  4. A renderer - this is where Three.js does the magic of rendering something we can look at based on all the geometry and lights and such

You can see where all these elements are initialized here, which generally boils down to:

Setting up the basics

initThreeJsScene: =>
  @scene = new THREE.Scene()

initThreeJsCamera: =>
  @camera = new THREE.PerspectiveCamera(
    50,   # Vertical field of view
    @targetElementWidth() / @targetElementHeight(), # aspect ratio
    0.1,  # Near plane
    1000  # Far plane
    )

  # Initial position of the camera
  @camera.position.x = -150
  @camera.position.y = 25

  # Point the camera to the origin
  @camera.lookAt( @scene.position )

initThreeJsRenderer: =>
  # WebGL renderer with transparent background
  @renderer = new THREE.WebGLRenderer(alpha: true)

  @renderer.setSize(@targetElementWidth(), @targetElementHeight())

  # Append the element created by the renderer onto out DOM structure
  @targetElement().appendChild( @renderer.domElement )

initThreeJsSceneLights: =>
  @scene.add( new THREE.AmbientLight( 0xffffff ) )

This sets up the basics for our project, we have our Scene, Camera, Light and Renderer . We pulled the camera a bit backwards on out x-axis and lifted it up just a tad to get some perspective and we setup the scene with an ambient white light so all objects are equally lit. At this point we don’t see anything given that we haven’t created any objects yet.

Setting up objects

Object wise we have two objects, the Rosetta spacecraft and the P67 comet, both of which are a .obj file that we can load into our scene using the OBJLoaded Three.js library.

loadTextures: =>
  @textures = {
    rosetta: new THREE.MeshBasicMaterial(color: 0x111111),
    comet: new THREE.Texture(),
    orbits: new THREE.LineBasicMaterial(color: 0xff00)
  }

  new THREE.ImageLoader().load 'assets/comet-texture.jpg', (image) =>
    @textures.comet.image = image
    @textures.comet.needsUpdate = true

The comet is loaded with a JPG texture and Rosetta has a single color, in this example it’s merely black.

loadObjects: =>
  loader = new THREE.OBJLoader()

  loader.load "assets/comet_67P.obj", @createComet
  loader.load "assets/rosetta_low_poly.obj", @createRosetta

createComet: (object) =>
  object.traverse (child) =>
    child.material.map = @textures.comet if child instanceof THREE.Mesh

  @objects.comet = object
  @objects.comet.scale.set(2, 2, 2)
  @scene.add(@objects.comet)

createRosetta: (object) =>
  object.traverse (child) =>
    child.material = @textures.rosetta if child instanceof THREE.Mesh

  @objects.rosetta = object
  @objects.rosetta.scale.set( 0.25, 0.25, 0.25 )
  @objects.rosetta.position.x = @ROSETTA_DISTANCE

  @objects.rosetta.matrixAutoUpdate = true
  @objects.rosetta.rotationAutoUpdate = true

  @scene.add(@objects.rosetta)

Loading the objects and applying the textures is pretty much boilerplate code, the only adjustments we do to each object are related to size and position. Scale wise I absolutely eyeballed it - with impressive scientific disregard - while in terms of position both objects are positioned at the origin by default, I merely more Rosetta to its relative position in relation to the comet to help with the animation code.

Animation loop

This is where we take care of positioning the objects in the scene and rendering each frame. We use a Three.Clock object to keep track of elapsed time and use that as a variable to calculate the position of the objects.

The loop itself runs using the requestAnimationFrame browser method, which gives it some control, namely it ensures that animations only run if you actually have the tab visible, so not to waste CPU cycles.

Each frame then simply calculates the position of each object and in the case of Rosetta, adds a very non-scientificly-accurate rotation for effect.

At the end of it we ask Three to render the scene and wait for the browser to call the animationLoop method again.

animationLoop: =>
  requestAnimationFrame( @animationLoop )
  if @objects.comet && @objects.rosetta
  @renderSceneOrbit()

renderSceneOrbit: =>
  time = @clock.getElapsedTime() / 8

  @calculateCometPosition(time)
  @calculateRosettaPosition(time)

  # Rotate objects
  @objects.rosetta.rotation.z = @objects.comet.rotation.z += 0.01
  @objects.rosetta.rotation.x = @objects.comet.rotation.x -= 0.01

  @renderer.render(@scene, @camera)

Calculating the positions

The comet position is pretty straightforward, as it merely rotates around the y axis, so with time as the variable we do

calculateCometPosition: (time) ->
  @objects.comet.position.x = Math.sin(time) * @ORBIT_RADIUS
  @objects.comet.position.z = Math.cos(time) * @ORBIT_RADIUS

The Rosetta spacecraft does a more complex movement, as it rotates around the comet as this moves through space. At this stage I’m happy to report I gained much more insight over trigonometry and vector operations than I ever had with only theoretical knowledge2, but I’m sure I’m still overcomplicating and possibly fumbling the whole of it, except that it generally looks good enough, inaccurate as I think it is. Anyway, this is what we do

calculateRosettaPosition: (time) ->
  # 1. Translation vector
  position = new THREE.Vector3(
    Math.cos(time * 5) * @ROSETTA_DISTANCE,
    Math.sin(time * 5) * @ROSETTA_DISTANCE,
    0
  )

  # 2. Rotate vector based on Comet position
  position.applyAxisAngle(
    new THREE.Vector3(0,1,0),
    Math.atan2(@objects.comet.position.z, @objects.comet.position.x)
  )

  # 3. Add comet position vector
  position.add(@objects.comet.position)

  # 4. Set Rosetta's position
  @objects.rosetta.position.x = position.x
  @objects.rosetta.position.y = position.y
  @objects.rosetta.position.z = position.z

I calculate the circular “orbit” movement of Rosetta, then rotate the point to match the angle of the comet and then simply add the comet’s current position to calculate the final point in space.

Extras

If you look at the console, there are a few settings that you can toggle, namely showing the world axis, a grid and drawing the movement of both objects over time. Nothing too mind-blowing there, you should pick it up super easily when checking the code itself, but just wanted to mentioning it here, so you know what to look - or how to interpret what’s going on in the actual code :)

Conclusion

This is a very simple yet specific intro based on a project I did. I hope it’s clear enough, if not hit me on twitter or Github itself. Three.js made things super easy, as you can see by the size of the boilerplate code - close to none - which is great as it means you can focus on the important things of your project.

I also like the way I can easily mingle this with the rest of the page however I want, which meant that I could use this animation as the header for the blog post, but it was also super easy to have it play on the listing page with a different size.

  1. aka, I clicked the first result for “webgl library” 

  2. there’s a whole rant hiding around here, in which I might also add Linear Algebra and other subjects that terrorized me for a while - but I’ll save you from all that by shutting up about it :)