Download Demo Programs

Do You See A Crate?

Three.js basics

Eric Haines

(template by Ilmari Heikkinen)

Who am I?

Eric Haines

erichaines.com | erich@acm.org | @pointinpolygon

Was a software engineer working on the Autodesk Viewer; now at NVIDIA.

Also developed and answers questions for this MOOC.



Download demos & slides at http://bit.ly/eric3demos

Or follow along at http://bit.ly/basics3js

Three.js?

Three.js is a JS 3D Engine

A Quick Demo

Musical Turk: http://ulysse.io/turk/

Repo at https://github.com/ucarion/turk

My variant: http://bit.ly/datanimusic


Bonus demo: "The Spirit" by Edan Kwan: http://bit.ly/edankwan

Starting Code

Was the View Black?

You probably have to allow file access.

Kill all your Chrome instances and restart with:
	chrome.exe --allow-file-access-from-files
It's a security thing.

See http://bit.ly/webglsetup for more information and other ways to work around this (e.g., start a local server).

Bonus info: debugger is F12 on Windows, Cmd-Shift-J on the Mac. Click on Console tab for warnings and errors.

The Existing Code

Look at BasicsOfThreeJS/three/aprog1.html, an example that comes with three.js.

Hit Ctrl-U to view the source code.

Create a Camera

camera = new THREE.PerspectiveCamera( 70,
    window.innerWidth / window.innerHeight, 1, 1000 );
camera.position.z = 400;

Make a Scene

scene = new THREE.Scene();

More Existing Code

Create a Cube with a Texture

var texture = new THREE.TextureLoader().load( 'textures/crate.gif' );

var geometry = new THREE.BoxBufferGeometry( 200, 200, 200 );
var material = new THREE.MeshBasicMaterial( { map: texture } );

mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );

A Bit More Existing Code

Hook up the Renderer:

renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
and some typical window resize code:
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize( window.innerWidth, window.innerHeight );
}

The Last Bit of Code

Make it move and render

function animate() {
	requestAnimationFrame( animate );

	mesh.rotation.x += 0.005;
	mesh.rotation.y += 0.01;

	renderer.render( scene, camera );
}

requestAnimationFrame?

requestAnimationFrame(function(time){}, element)

It's like setTimeout(f, timeUntilNextFrame) except that:

Always use this for best efficiency, especially for mobile.

Time To Code

First Step

Code: BasicsOfThreeJS/three/aprog1.html

Turn off the rotations; comment them out:

function animate() {
	requestAnimationFrame( animate );

	//mesh.rotation.x += 0.005;
	//mesh.rotation.y += 0.01;

	renderer.render( scene, camera );
}

Edit in your favorite text editor, save, and refresh the browser (F5 on Windows; Cmd-R on Mac) to see the result.

Now It's Stopped

Hook up a Mouse Control

Code: BasicsOfThreeJS/three/aprog2.html

Part one, add controls line at the end, since we're here:

function animate() {
	requestAnimationFrame( animate );

	//mesh.rotation.x += 0.005;
	//mesh.rotation.y += 0.01;

	renderer.render( scene, camera );
	
	controls.update();	// add this
}

Hook up a Mouse Control 2

At top, include OrbitControls.js script, and declare controls:

<body>
	<script src="three.js/build/three.js"></script>
	<script src="js/controls/OrbitControls.js"></script>

	<script>

	    var controls;	// add this
This is one of a number of controls Three.js provides.

Hook up a Mouse Control 3

Part three, create the controls:

renderer.setSize( window.innerWidth, window.innerHeight );

// create the controls - add this
controls = new THREE.OrbitControls( camera, renderer.domElement );
This associates the mouse with the camera. Try the left/right/middle buttons and mouse wheel.

OrbitControls has lots of options to customize behavior. Also works with touch controls for mobile.

Mouse Affects Camera

Let's Use a Different Texture

Code: BasicsOfThreeJS/three/aprog3.html

Change the texture name:

// how the line used to look:
//var texture = new THREE.TextureLoader().load( 
//	'textures/crate.gif' );

var texture = new THREE.TextureLoader().load(
	'textures/UV_Grid_Sm.jpg' ); // new file name

Also, let's change the background color:

renderer = new THREE.WebGLRenderer();

// set the background color to gray: a0 is R,G,B; a0=10*16+0 hex=160
renderer.setClearColor( 0xa0a0a0 );

New Texture & Background

Lighting

Let's Add a Light

Code: BasicsOfThreeJS/three/aprog4.html

Get typing:

scene = new THREE.Scene();

// create a light
light = new THREE.DirectionalLight(0xffffff);
light.position.set(100,200,400);
scene.add(light);

And Change the Material

A one-word change, "Basic" to "Phong" or "Standard":

// material = new THREE.MeshBasicMaterial( { map: texture } );
// to have the lights affect the material, use Phong instead of Basic
material = new THREE.MeshPhongMaterial( { map: texture } );

Another option is the new physically-based material (PBR):

// material = new THREE.MeshBasicMaterial( { map: texture } );
// to have the lights affect the material, use Phong instead of Basic
material = new THREE.MeshStandardMaterial( { map: texture } );

Let There Be Light!

Let's Add More Lights

Code: BasicsOfThreeJS/three/aprog5.html

Copy and paste the original light and modify:

light2 = new THREE.DirectionalLight(0xffffff);
light2.position.set(-400,200,-400);

scene.add(light2);

light3 = new THREE.DirectionalLight(0xffffff);
light3.position.set(100,-400,-400);

scene.add(light3);
This is bad practice, BTW: I didn't declare these variables.

Let There Be More Lights!

Positioning

Let's Add A Ground Plane

Code: BasicsOfThreeJS/three/aprog6.html

Add an object:

scene.add(light3);

var geometry = new THREE.PlaneGeometry( 1000, 1000 );

var material = new THREE.MeshPhongMaterial(
    {color: 0xffff00, side: THREE.DoubleSide} );
	
var plane = new THREE.Mesh( geometry, material );

scene.add( plane );

A Ground Plane, Almost

Rotate the Plane...

Code: BasicsOfThreeJS/three/aprog7.html

But along what axis? Let's add this:

scene.add( plane );

// add XYZ axes, in RGB colors
var axisHelper = new THREE.AxisHelper( 200 );
scene.add( axisHelper );
Adds an XYZ set of axes, with colors RGB

A Plane and Axes

Rotate the Plane

Code: BasicsOfThreeJS/three/aprog8.html

Rotations are in radians:

var plane = new THREE.Mesh( geometry, material );
plane.rotation.x = 90 * Math.PI / 180;	// add this

scene.add( plane );
Multiplying by Math.PI / 180 converts degrees to radians.

A Plane, Rotated

Position the Plane

Code: BasicsOfThreeJS/three/aprog9.html

Green is the Y axis

var plane = new THREE.Mesh( geometry, material );
plane.rotation.x = 90 * Math.PI / 180;	// add this
plane.position.y = -100.01;

scene.add( plane );
The 0.01 is to put the plane just below the box to avoid z-fighting.

Ground Plane, At Last

GUI

Interactive Controls

Code: BasicsOfThreeJS/three/aprog10.html

Add the dat.gui library to the top, and definitions:

<script src="three.js/build/three.js"></script>
<script src="js/controls/OrbitControls.js"></script>
<script src="js/libs/dat.gui.min.js"></script>

<script>
	"use strict";
	var controls;
	var light, light2, light3, params;	// add this
Adding the code: "use strict"; at the top of the Javascript can help catch errors.

Let's Adjust the Lights

Typing time:

window.addEventListener( 'resize', onWindowResize, false );

// add all this to create a slider
params = {intensity: 1};				
var gui = new dat.GUI();

gui.add( params, 'intensity', 0, 2 );
gui.open();
This makes a slider that changes params.intensity

Let's Adjust the Lights, Part 2

Use the slider variable params.intensity:

function animate() {
	requestAnimationFrame( animate );

	// have our slider for intensity affect the lighting
	light.intensity = light2.intensity = light3.intensity = 
		params.intensity;

	renderer.render( scene, camera );
	controls.update();
}

Light Dimmer Switch

I'm Sick of the Box

Code: BasicsOfThreeJS/three/aprog11.html

Change it to a sphere:

//var geometry = new THREE.BoxBufferGeometry( 200, 200, 200 );
// let's use a sphere instead of a box
var geometry = new THREE.SphereGeometry( 100, 32, 16 );
100 is the radius, 32 x 16 is the mesh formed.

See the mesh by changing the material to:

material = new THREE.MeshPhongMaterial( { wireframe : true } );

The Sphere

Animation

Let's Make It Bounce

Code: BasicsOfThreeJS/three/aprog12.html

Add a line to change the mesh's vertical position:

function animate() {
	requestAnimationFrame( animate );

	// have our slider for intensity affect the lighting
	light.intensity = light2.intensity = light3.intensity = 
		params.intensity;

	// bounce				
	mesh.position.y =
		100 * Math.abs(Math.cos(Date.now() * 0.005));

	renderer.render( scene, camera );
	controls.update();
}

Bouncy Ball

What's Chopping That Corner?

Code: BasicsOfThreeJS/three/aprog13.html

The camera has a near and far plane to it. Move the far plane out a bit, from 1000 to 2000:

function init() {

	camera = new THREE.PerspectiveCamera(
		70, window.innerWidth / window.innerHeight, 
		1, 2000 );	// near and far plane distances
Pro tip: move the near plane out as far as you can, move the far fairly close. Near matters more.

Fixed Far Clipping

Debugging

Is just F12 away

for example

Debugging Tools

F12 on Windows, Cmd-Shift-J on the Mac

http://jshint.com / ESLint are handy.
Website / plug-in for various text editors.



Spector.js is a WegGL tool helpful for watching things draw.



Lots more tools listed at http://bit.ly/webglhelp.

Other Good Stuff

(some slides by Ilmari Heikkinen)

How about Shadows?

Three.js has shadow maps.

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

The shadows work on only spot and directional lights.

Code from BasicsOfThreeJS/three/three_5.html:

      // enable shadows on the renderer
      renderer.shadowMap.enabled = true;

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

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

Here's a Different Example

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 (code).

Projects geometry to screen coordinates.

    <script id="vertex" type="x-shader/x-vertex">
      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="x-shader/x-fragment">
      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>

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: document.getElementById( 'vertex' ).textContent,
  fragmentShader: document.getElementById( 'fragment' ).textContent
});

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

It's very colorful

How to select?

Shoot a view ray and find intersecting objects (code).

raycaster.setFromCamera( mouse, camera );
var intersects = raycaster.intersectObjects( scene.children );
if ( intersects.length > 0 ) {
    if ( INTRSCT != intersects[ 0 ].object ) {
        if ( INTRSCT )
            INTRSCT.material.emissive.setHex( INTRSCT.currentHex );
        INTRSCT = intersects[ 0 ].object;
        INTRSCT.currentHex = INTRSCT.material.emissive.getHex();
        INTRSCT.material.emissive.setHex( 0xff0000 );
    }
} else {
    if ( INTRSCT )
        INTRSCT.material.emissive.setHex( INTRSCT.currentHex );
    INTRSCT = null;
}

WebGL Interactive Cubes

Loading models

Model exporters

Look into utils/exporters/

Model loaders

Look into examples/js/loaders/

And many more. 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 (program).

Monster

Practical 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

Conclusion

Three.js Ecosystem

It's JavaScript

They're on release 95.

They attempt to make the API perfect

   This can be good, e.g. ES2015

   But, they're not big on backward compatibility

Documentation is sometimes "TODO"

   Pro tip: http://stemkoski.github.io/Three.js/

Three.js Summary

threejs.org

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

Eric Haines

erichaines.com | erich@acm.org | @pointinpolygon


Slides at http://bit.ly/eric3demos (github.com/erich666/BasicsOfThreeJS)


A full free course I created: http://bit.ly/intro3D

My WebGL resource page: http://bit.ly/webglhelp


Free WebGL book: http://webglinsights.com