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