WebGL With Three.js – Lesson 1
As we have promised – it is time to start our new series of articles devoted to the WebGL. This is our first lesson, where we consider the main basic functions: creating a scene, camera, renderer, controls (OrbitControls). We will also create the simplest directional light, add a dozen objects (of different geometry) with shadows. In order to make things went faster, we decided to take one of the most popular webgl frameworks – three.js. Why use Three.js? Besides the fact that it is open source JavaScript framework, it is also the most rapidly growing and discussed engine. Here already implemented really many possibilities, from low-level work with points and vectors, to work with ready scenes, shaders and even stereoscopic effects.
Live Demo
HTML
We could omit this step, but, habitually, we do it at every lesson. So here’s the html structure of our lesson:
<!DOCTYPE html> <html lang="en" > <head> <meta charset="utf-8" /> <meta name="author" content="Script Tutorials" /> <title>WebGL With Three.js - Lesson 1 | Script Tutorials</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <link href="css/main.css" rel="stylesheet" type="text/css" /> </head> <body> <script src="js/three.min.js"></script> <script src="js/THREEx.WindowResize.js"></script> <script src="js/OrbitControls.js"></script> <script src="js/stats.min.js"></script> <script src="js/script.js"></script> </body> </html>
In this code, we just connect all the libraries that we are going to use today.
Javascript
I hope that you have already seen our demo and imagine what the basic elements it is composed, we will consider the creation of each item step-by-step.
Skeleton
Abstractly, our scene looks like this:
var lesson1 = { scene: null, camera: null, renderer: null, container: null, controls: null, clock: null, stats: null, init: function() { // Initialization } }; // Animate the scene function animate() { requestAnimationFrame(animate); render(); update(); } // Update controls and stats function update() { lesson1.controls.update(lesson1.clock.getDelta()); lesson1.stats.update(); } // Render the scene function render() { if (lesson1.renderer) { lesson1.renderer.render(lesson1.scene, lesson1.camera); } } // Initialize lesson on page load function initializeLesson() { lesson1.init(); animate(); } if (window.addEventListener) window.addEventListener('load', initializeLesson, false); else if (window.attachEvent) window.attachEvent('onload', initializeLesson); else window.onload = initializeLesson;
This is common structure of application built with three.js. Almost everything will be created in the ‘init’ function.
Creation of scene, camera and renderer
They are the main elements of our scene, they following code creates an empty scene with perspective camera and renderer with enabled shadow map:
// create main scene this.scene = new THREE.Scene(); var SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight; // prepare camera var VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 1, FAR = 10000; this.camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR); this.scene.add(this.camera); this.camera.position.set(-1000, 1000, 0); this.camera.lookAt(new THREE.Vector3(0,0,0)); // prepare renderer this.renderer = new THREE.WebGLRenderer({antialias:true, alpha: false}); this.renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT); this.renderer.setClearColor(0xffffff); this.renderer.shadowMapEnabled = true; this.renderer.shadowMapSoft = true; // prepare container this.container = document.createElement('div'); document.body.appendChild(this.container); this.container.appendChild(this.renderer.domElement); // events THREEx.WindowResize(this.renderer, this.camera);
We set the camera at an angle of 45 degrees, set the full screen size and white cleanup color for WebGLRenderer, added our scene in the HTML document, and also connected THREEx.WindowResize to control the renderer and the camera when resizing the window (of browser).
OrbitControls and Stats
In order to be able to somehow control the camera – three.js gives us the possibility to use ready-made controls. One of these is OrbitControls, which provides the ability to rotate the scene around its axis. Also it will be helpful to see the statistics (FPS) of our scene – a very small class Stats will help us (stats.min.js).
// prepare controls (OrbitControls) this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement); this.controls.target = new THREE.Vector3(0, 0, 0); // prepare clock this.clock = new THREE.Clock(); // prepare stats this.stats = new Stats(); this.stats.domElement.style.position = 'absolute'; this.stats.domElement.style.bottom = '0px'; this.stats.domElement.style.zIndex = 10; this.container.appendChild( this.stats.domElement );
Thus we have prepared four materials
Creation of light and ground
Light is one of the important elements of the scene, in our first tutorial we will create the most simple – directional light, since we are going to add basic shadows:
// add directional light var dLight = new THREE.DirectionalLight(0xffffff); dLight.position.set(1, 1000, 1); dLight.castShadow = true; dLight.shadowCameraVisible = true; dLight.shadowDarkness = 0.2; dLight.shadowMapWidth = dLight.shadowMapHeight = 1000; this.scene.add(dLight); // add particle of light particleLight = new THREE.Mesh( new THREE.SphereGeometry(10, 10, 10), new THREE.MeshBasicMaterial({ color: 0x44ff44 })); particleLight.position = dLight.position; this.scene.add(particleLight); // add simple ground var groundGeometry = new THREE.PlaneGeometry(1000, 1000, 1, 1); ground = new THREE.Mesh(groundGeometry, new THREE.MeshLambertMaterial({ color: this.getRandColor() })); ground.position.y = 0; ground.rotation.x = - Math.PI / 2; ground.receiveShadow = true; this.scene.add(ground);
When we create the light, we enabled two parameters (castShadow and shadowCameraVisible). This will allow us to visually see where is the light and we can understand the process of creating (and boundaries) of shadows.
You may also notice that immediately after the light, we have added a spherical object – it is for you, to be able to visually know in what position is our directional light source. In order to create the earth – we used the flat element (PlaneGeometry), to prepare it to receiveshadows – we set the ‘receiveShadow’ param to ‘true’.
Colors
Soon we will add additional objects in the scene. But, I prepared one additional function to generate a different color for our items. This function will randomly return a color from a predefined list of pastel colors.
var colors = [ 0xFF62B0, 0x9A03FE, 0x62D0FF, 0x48FB0D, 0xDFA800, 0xC27E3A, 0x990099, 0x9669FE, 0x23819C, 0x01F33E, 0xB6BA18, 0xFF800D, 0xB96F6F, 0x4A9586 ]; getRandColor: function() { return colors[Math.floor(Math.random() * colors.length)]; }
CircleGeometry / CubeGeometry / CylinderGeometry and ExtrudeGeometry
Geometry is an object to hold all necessary data to describe any 3D model (points, vertices, faces etc). All the listed geometries are used to create such objects as: circle (plain), cube and cylinder. Extrude Geometry is used to extrude geometry from a path shape. We are going to extrude triangle:
// add circle shape var circle = new THREE.Mesh(new THREE.CircleGeometry(70, 50), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); circle.rotation.x = - Math.PI / 2; circle.rotation.y = - Math.PI / 3; circle.rotation.z = Math.PI / 3; circle.position.x = -300; circle.position.y = 150; circle.position.z = -300; circle.castShadow = circle.receiveShadow = true; this.scene.add(circle); // add cube shape var cube = new THREE.Mesh(new THREE.CubeGeometry(100, 100, 100), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); cube.rotation.x = cube.rotation.z = Math.PI * 0.1; cube.position.x = -300; cube.position.y = 150; cube.position.z = -100; cube.castShadow = cube.receiveShadow = true; this.scene.add(cube); // add cylinder shape var cube = new THREE.Mesh(new THREE.CylinderGeometry(60, 80, 90, 32), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); cube.rotation.x = cube.rotation.z = Math.PI * 0.1; cube.position.x = -300; cube.position.y = 150; cube.position.z = 100; cube.castShadow = cube.receiveShadow = true; this.scene.add(cube); // add extrude geometry shape var extrudeSettings = { amount: 10, steps: 10, bevelSegments: 10, bevelSize: 10, bevelThickness: 10 }; var triangleShape = new THREE.Shape(); triangleShape.moveTo( 0, -50 ); triangleShape.lineTo( -50, 50 ); triangleShape.lineTo( 50, 50 ); triangleShape.lineTo( 0, -50 ); var extrude = new THREE.Mesh(new THREE.ExtrudeGeometry(triangleShape, extrudeSettings), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); extrude.rotation.y = Math.PI / 2; extrude.position.x = -300; extrude.position.y = 150; extrude.position.z = 300; extrude.castShadow = extrude.receiveShadow = true; this.scene.add(extrude);
Once the geometry is created, we can create a Mesh on the basis of this geometry. When you create a Mesh, we specify the material (as the second argument).
IcosahedronGeometry / OctahedronGeometry / RingGeometry and ShapeGeometry
Now we will create the next four elements: icosahedron, octahedron, ring, and the object by a custom path (shape) using ShapeGeometry:
// add icosahedron shape var icosahedron = new THREE.Mesh(new THREE.IcosahedronGeometry(70), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); icosahedron.position.x = -100; icosahedron.position.y = 150; icosahedron.position.z = -300; icosahedron.castShadow = icosahedron.receiveShadow = true; this.scene.add(icosahedron); // add octahedron shape var octahedron = new THREE.Mesh(new THREE.OctahedronGeometry(70), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); octahedron.position.x = -100; octahedron.position.y = 150; octahedron.position.z = -100; octahedron.castShadow = octahedron.receiveShadow = true; this.scene.add(octahedron); // add ring shape var ring = new THREE.Mesh(new THREE.RingGeometry(30, 70, 32), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); ring.rotation.y = -Math.PI / 2; ring.position.x = -100; ring.position.y = 150; ring.position.z = 100; ring.castShadow = ring.receiveShadow = true; this.scene.add(ring); // add shape geometry shape var shapeG = new THREE.Mesh(new THREE.ShapeGeometry(triangleShape), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); shapeG.rotation.y = -Math.PI / 2; shapeG.position.x = -100; shapeG.position.y = 150; shapeG.position.z = 300; shapeG.castShadow = shapeG.receiveShadow = true; this.scene.add(shapeG);
SphereGeometry / TetrahedronGeometry / TorusGeometry / TorusKnotGeometry and TubeGeometry
Finally, we create a sphere, tetrahedron, torus, torus knot and tube:
// add sphere shape var sphere = new THREE.Mesh(new THREE.SphereGeometry(70, 32, 32), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); sphere.rotation.y = -Math.PI / 2; sphere.position.x = 100; sphere.position.y = 150; sphere.position.z = -300; sphere.castShadow = sphere.receiveShadow = true; this.scene.add(sphere); // add tetrahedron shape var tetrahedron = new THREE.Mesh(new THREE.TetrahedronGeometry(70), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); tetrahedron.position.x = 100; tetrahedron.position.y = 150; tetrahedron.position.z = -100; tetrahedron.castShadow = tetrahedron.receiveShadow = true; this.scene.add(tetrahedron); // add torus shape var torus = new THREE.Mesh(new THREE.TorusGeometry(70, 20, 16, 100), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); torus.rotation.y = -Math.PI / 2; torus.position.x = 100; torus.position.y = 150; torus.position.z = 100; torus.castShadow = torus.receiveShadow = true; this.scene.add(torus); // add torus knot shape var torusK = new THREE.Mesh(new THREE.TorusKnotGeometry(70, 20, 16, 100), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); torusK.rotation.y = -Math.PI / 2; torusK.position.x = 100; torusK.position.y = 150; torusK.position.z = 300; torusK.castShadow = torusK.receiveShadow = true; this.scene.add(torusK); // add tube shape var points = []; for (var i = 0; i < 10; i++) { var randomX = -100 + Math.round(Math.random() * 200); var randomY = -100 + Math.round(Math.random() * 200); var randomZ = -100 + Math.round(Math.random() * 200); points.push(new THREE.Vector3(randomX, randomY, randomZ)); } var tube = new THREE.Mesh(new THREE.TubeGeometry(new THREE.SplineCurve3(points), 64, 20), new THREE.MeshLambertMaterial({ color: this.getRandColor() })); tube.rotation.y = -Math.PI / 2; tube.position.x = 0; tube.position.y = 500; tube.position.z = 0; tube.castShadow = tube.receiveShadow = true; this.scene.add(tube);
Pay attention to the process of creating the pipe. TubeGeometry allows us to build a cylindrical object by the array of points.
Smooth movement of the light
In order to smoothly move the light, we only need to add the following code in the ‘update’ function:
// smoothly move the particleLight var timer = Date.now() * 0.000025; particleLight.position.x = Math.sin(timer * 5) * 300; particleLight.position.z = Math.cos(timer * 5) * 300;
Live Demo
[sociallocker]
download in package
[/sociallocker]
Conclusion
I think that we done enough for our first lesson. In addition to creating a scene, we also saw how to create a shadow, and even made a review of almost all possible geometries. See you at the next lesson!