
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
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();
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);
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)
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());
requestAnimationFrame(function(time){}, element)
It's like setTimeout(f, timeUntilNextFrame)
Except that:
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);
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;
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;
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 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.
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>
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>
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
);
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);
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);
}
}
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);
Double-click to animate
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;
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);
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!
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.
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);
Look into utils/exporters/
Look into src/extras/loaders/
Huh? So what should I use?
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.
JavaScript library for 3D graphics
Easy to use
Efficient
Nice feature set
DAT.GUI for simple GUIs code.google.com/p/dat-gui
Ilmari Heikkinen
@ilmarihei | fhtr.org/plus | fhtr.org
Slides available at fhtr.org/BasicsOfThreeJS
Repo at github.com/kig/BasicsOfThreeJS