Three.js basics

Ilmari Heikkinen

Who am I?

Ilmari Heikkinen

@ilmarihei | fhtr.org/plus | fhtr.org

Google Chrome Developer Programs Engineer



I write demos, do presentations, write articles



Slides available at fhtr.org/BasicsOfThreeJS

Repo at github.com/kig/BasicsOfThreeJS

Three.js?

Three.js is a JS 3D Engine

Basic setup

Renderer

Create a WebGLRenderer

      var renderer = new THREE.WebGLRenderer({antialias: true});
      renderer.setSize(document.body.clientWidth, 
                       document.body.clientHeight);

Plug it in

      document.body.appendChild(renderer.domElement);

And make it pretty

      renderer.setClearColorHex(0xEEEEEE, 1.0);
      renderer.clear();

Wow!

It gets better, I promise!

Create a Camera

// new THREE.PerspectiveCamera( FOV, viewAspectRatio, zNear, zFar );
var camera = new THREE.PerspectiveCamera(45, width/height, 1, 10000);
camera.position.z = 300;

Make a Scene with a Cube

var scene = new THREE.Scene();
var cube = new THREE.Mesh(new THREE.CubeGeometry(50,50,50),
               new THREE.MeshBasicMaterial({color: 0x000000}));
scene.add(cube);

And render the Scene from the Camera

renderer.render(scene, camera);

At least it isn't empty.

Recap

Create a renderer new THREE.WebGLRenderer()

Create a camera new THREE.PerspectiveCamera(fov, aR, n, f)

Create a scene new THREE.Scene()

Create a mesh with a geometry and a material
new THREE.Mesh(new THREE.CubeGeometry(w,h,d),
          new THREE.MeshBasicMaterial({color: 0x000000}))

Add the mesh to the scene scene.add(mesh)

Render the scene renderer.render(scene, camera)

Add the renderer canvas to the page document.body.appendChild(renderer.domElement)

Animation

Can we make it move?

Yes we can!

(But it's a manual process.)

      function animate(t) {
        // spin the camera in a circle
        camera.position.x = Math.sin(t/1000)*300;
        camera.position.y = 150;
        camera.position.z = Math.cos(t/1000)*300;
        // you need to update lookAt every frame
        camera.lookAt(scene.position);
        // renderer automatically clears unless autoClear = false
        renderer.render(scene, camera);
        window.requestAnimationFrame(animate, renderer.domElement);
      };
      animate(new Date().getTime());
    

Isn't it nice?

requestAnimationFrame?

requestAnimationFrame(function(time){}, element)

It's like setTimeout(f, timeUntilNextFrame)

Except that:

Lighting

Lights

Without light, our scene is very forlorn.

Let's create a Light

      var light = new THREE.SpotLight();
      light.position.set( 170, 330, -160 );
      scene.add(light);

And a lit cube

      var litCube = new THREE.Mesh(
        new THREE.CubeGeometry(50, 50, 50),
        new THREE.MeshLambertMaterial({color: 0xFFFFFF}));
      litCube.position.y = 50;
      scene.add(litCube);

There we go!

How about shadows?

Three.js has shadow maps.

You need to enable them per-light and per-object.

The shadows only work on SpotLights.

      // enable shadows on the renderer
      renderer.shadowMapEnabled = true;

      // enable shadows for a light
      light.castShadow = true;

      // enable shadows for an object
      litCube.castShadow = true;
      litCube.receiveShadow = true;

Added interest

Let's add a ground plane

  var planeGeo = new THREE.PlaneGeometry(400, 200, 10, 10);
  var planeMat = new THREE.MeshLambertMaterial({color: 0xFFFFFF});
  var plane = new THREE.Mesh(planeGeo, planeMat);
  plane.rotation.x = -Math.PI/2;
  plane.position.y = -25;
  plane.receiveShadow = true;
  scene.add(plane);

And make the cube spin

  litCube.position.x = Math.cos(t/600)*85;
  litCube.position.y = 60-Math.sin(t/900)*25;
  litCube.position.z = Math.sin(t/600)*85;
  litCube.rotation.x = t/500;
  litCube.rotation.y = t/800;

Plus a couple of tweaks

Lighting recap

Set mesh material to new THREE.MeshLambertMaterial or new THREE.MeshPhongMaterial

Create a light new THREE.SpotLight(color)

Add the light to the scene scene.add(light)

Turn on shadows if you need them

      renderer.shadowMapEnabled = true;
      light.castShadow = true;
      object.castShadow = true;
      object.receiveShadow = true;

Shaders

What are shaders

Shaders are small programs that tell WebGL


where to draw

and

what to draw

Shaders are written in GLSL, the GL Shading Language.

It's kinda like C for graphics.

Vertex shader

Where to draw.

Projects geometry to screen coordinates.

    <script id="vertex" type="text/x-glsl-vert">
      varying float vZ;
      uniform float time;
      void main() {
        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
        mvPosition.y += 20.0*sin(time*0.5+mvPosition.x/25.0);
        mvPosition.x += 30.0*cos(time*0.5+mvPosition.y/25.0);
        vec4 p = projectionMatrix * mvPosition;
        vZ = p.z;
        gl_Position = p;
      }
    </script>

Fragment shader

What to draw.

Computes the color of a pixel.

    <script id="fragment" type="text/x-glsl-frag">
      varying float vZ;
      uniform float time;
      uniform vec2 size;
      void main() {
        vec2 d = gl_FragCoord.xy - (0.5+0.02*sin(time))*size;
        float a = sin(time*0.3)*2.0*3.14159;
        d = vec2( d.x*cos(a) + d.y*sin(a),
                 -d.x*sin(a) + d.y*cos(a));
        vec2 rg = vec2(1.0)-abs(d)/(0.5*size)
        float b = abs(vZ) / 160.0;
        gl_FragColor = vec4(rg,b,1.0);
      }
    </script>

It's very colourful

Shaders and Three.js

Use a ShaderMaterial

var uniforms = {
  time : { type: "f", value: 1.0 },
  size : { type: "v2", value: new THREE.Vector2(width,height) }
};

var shaderMaterial = new THREE.ShaderMaterial({
  uniforms : uniforms,
  vertexShader : $('#vertex').text(),
  fragmentShader : $('#fragment').text()
});

var meshCube = new THREE.Mesh(
  new THREE.CubeGeometry(50,50,50, 20,20,20), // 20 segments
  shaderMaterial
);

Animating shader uniforms

Change the uniform value

      uniforms.time.value += 0.05;

And for the size, add a resize listener

      window.addEventListener('resize', function() {
        uniforms.size.value.x = window.innerWidth;
        uniforms.size.value.y = window.innerHeight;
      }, false);

Useful uses

Bar graph

      var grid = /* 2D Array */
      var barGraph = new THREE.Object3D();
      scene.add(barGraph);

      var max = /* Grid max value */
      var mat = new THREE.MeshLambertMaterial({color: 0xFFAA55});
      for (var j=0; j<grid.length; j++) {
        for (var i=0; i<grid[j].length; i++) {
          var barHeight = grid[j][i]/max * 80;
          var geo = new THREE.CubeGeometry(8, barHeight, 8);
          var mesh = new THREE.Mesh(geo, mat);
          mesh.position.x = (i-grid[j].length/2) * 16;
          mesh.position.y = barHeight/2;
          mesh.position.z = -(j-grid.length/2) * 16;
          mesh.castShadow = mesh.receiveShadow = true;
          barGraph.add(mesh);
        }
      }

Bar graph in action

Scatter plot

var scatterPlot = new THREE.Object3D();
var mat = new THREE.ParticleBasicMaterial(
  {vertexColors: true, size: 1.5});

var pointCount = 10000;
var pointGeo = new THREE.Geometry();
for (var i=0; i<pointCount; i++) {
  var x = Math.random() * 100 - 50;
  var y = x*0.8+Math.random() * 20 - 10;
  var z = x*0.7+Math.random() * 30 - 15;
  pointGeo.vertices.push(new THREE.Vertex(new THREE.Vector3(x,y,z)));
  pointGeo.colors.push(new THREE.Color().setHSV(
    (x+50)/100, (z+50)/100, (y+50)/100));
}
var points = new THREE.ParticleSystem(pointGeo, mat);
scatterPlot.add(points);
scene.fog = new THREE.FogExp2(0xFFFFFF, 0.0035);

Scatter plot in action

Double-click to animate

How to do text

Create a canvas, draw text on it, use as texture.

      var c = document.createElement('canvas');
      c.getContext('2d').font = '50px Arial';
      c.getContext('2d').fillText('Hello, world!', 2, 50);

      var tex = new THREE.Texture(c);
      tex.needsUpdate = true;
      
      var mat = new THREE.MeshBasicMaterial({map: tex});
      mat.transparent = true;

      var titleQuad = new THREE.Mesh(
        new THREE.PlaneGeometry(c.width, c.height),
        mat
      );
      titleQuad.doubleSided = true;

Hello, world!

And the lines?

Create a geometry, add vertices, use to make a THREE.Line.

      function v(x,y,z){ 
        return new THREE.Vertex(new THREE.Vector3(x,y,z)); 
      }
      
      var lineGeo = new THREE.Geometry();
      lineGeo.vertices.push(
        v(-50, 0, 0), v(50, 0, 0),
        v(0, -50, 0), v(0, 50, 0),
        v(0, 0, -50), v(0, 0, 50)
      );
      var lineMat = new THREE.LineBasicMaterial({
        color: 0x000000, lineWidth: 1});
      var line = new THREE.Line(lineGeo, lineMat);
      line.type = THREE.Lines;
      scene.add(line);

Coordinate axes

GUI controls

GUI controls

To me, GUI controls have been a pain.

Write your own widgets, write your own onchange handlers, write your own interval change polling logic, style it all, hope it scales, gaaaaaah!

Let's just use DAT.GUI instead.

      var gui = new DAT.GUI();
      gui.add(cube.scale, 'x').min(0.1).max(10).step(0.1);
      gui.add(cube.scale, 'y', 0.1, 10, 0.1);
      gui.add(cube.scale, 'z', 0.1, 10, 0.1);

Done!

code.google.com/p/dat-gui

Scalable cube

But but, where is the pain?

Well, we could do a proxy to control only the currently selected object.

var controller = new THREE.Object3D();
var gui = new DAT.GUI({width: 160});

controller.setCurrent = function(current) {
  this.current = current;
  this.x.setValue(current.position.x);
  this.y.setValue(current.position.y);
  this.z.setValue(current.position.z);
};
      
controller.x = gui.add(controller.position, 'x').onChange(function(v){
  controller.current.position.x = v;
});
// etc.

How to select?

Project a ray into the scene and find intersecting objects.

var projector = new THREE.Projector();
window.addEventListener('mousedown', function (ev){
  if (ev.target == renderer.domElement) {
    var x = ev.clientX;
    var y = ev.clientY;
    var v = new THREE.Vector3((x/width)*2-1, -(y/height)*2+1, 0.5);
    projector.unprojectVector(v, camera);
    var ray = new THREE.Ray(camera.position, 
                            v.subSelf(camera.position).normalize());
    var intersects = ray.intersectObjects(controller.objects);
    if (intersects.length > 0) {
      controller.setCurrent(intersects[0].object);
    }
  }
}, false);

Great cubistan

Loading models

Model exporters

Look into utils/exporters/

Model loaders

Look into src/extras/loaders/

Huh? So what should I use?

COLLADA loader

new THREE.ColladaLoader().load('models/monster.dae',
function(collada) {
  var model = collada.scene;
  model.scale.set(0.1, 0.1, 0.1);
  model.rotation.x = -Math.PI/2;
  scene.add(model);
});

Doesn't look too complicated.

Copy-pasted from examples/webgl_collada.html

Along with the code to make it animate.

Monster

Conclusion

Three.js

github.com/mrdoob/three.js

JavaScript library for 3D graphics

Easy to use

Efficient

Nice feature set

DAT.GUI for simple GUIs code.google.com/p/dat-gui

The End

Ilmari Heikkinen

@ilmarihei | fhtr.org/plus | fhtr.org



Slides available at fhtr.org/BasicsOfThreeJS

Repo at github.com/kig/BasicsOfThreeJS