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:
- A Scene - this will hold our objects, our lights and our camera.
- 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.
- The objects and materials themselves - in our case, one comet with texture and a Rosetta spacecraft with a simple color.
- 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.