Today’s article refers to the Christmas and new year in the most direct manner. I prepared a remarkable and relevant demonstration of possibilities of three.js library in the form of an interactive Christmas card. This postcard has everything we need – the Christmas tree with toys, the star in the top, snow, snowflakes in the air – all to raise new year spirit of Christmas time. In this tutorial I will show you how to work with 3D scene, fog, cameras, textures, materials, basic objects (meshes), ground, lights, particles and so on.
Our result works good for all modern browsers (using the HTML5 technology). To start, let’s look at the result and online demo:
First of all, I want to say a few words about three.js. Today’s lesson is the first where we use this library, which is really amazing, because it lets you get really good results with the minimum of effort. Almost all the necessary things for creating 3d scenes have already been implemented in this library, which harnesses the power of WebGL. I think that next year we will come back to WebGL more than once.
Now, if you are ready to develop it – let’s start. In the beginning, create a blank html file (index.html) and put the following code:
04 | <meta charset="utf-8" /> |
05 | <meta name="author" content="Script Tutorials" /> |
06 | <title>Christmas tree with three.js | Script Tutorials</title> |
07 | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
09 | <link href="css/main.css" rel="stylesheet" type="text/css" /> |
13 | <script src="js/three.min.js"></script> |
14 | <script src="js/script.js"></script> |
Could it be simpler? Indeed, all you see is the basic HTML markup with few meta tags, and two javascript files, the first of which is the library itself (three.min.js), and the second file with the code of our Christmas card (script.js). Pay attention, that both javascript files are in the ‘js’ folder. Don’t forget to create this folder (‘js’) in the same folder where you saved your index.html file.
Next step – you will need to download three.min.js library. You can find it in the Internet, and it is also available here. This file should be placed in the ‘js’ folder which you prepared earlier. Starting from the current moment we begin to understand the process of creation of all the scene elements. Now create a new blank javascript file, save it in the ‘js’ folder with the ‘script.js’ name. And let’s start from the beginning
Skeleton
To begin, we define the global variables and the application skeleton
02 | var camera, scene, renderer, group; |
03 | var targetRotation = 0; |
04 | var targetRotationOnMouseDown = 0; |
06 | var mouseXOnMouseDown = 0; |
07 | var windowHalfX = window.innerWidth / 2; |
08 | var windowHalfY = window.innerHeight / 2; |
15 | requestAnimationFrame(animate); |
19 | renderer.render(scene, camera); |
This is fairly common structure of application built with three.js. Almost everything will be initialized and created in the ‘init’ function. Then we will add few more function for user interaction.
Scene, camera, action
This is a preparatory stage on which we prepare container for our scene, display some intro description, initialize the scene, fog, camera, and then we prepare the group (THREE.Object3D) on which we will put elements of the scene. Add the following code in the ‘init’ function:
02 | container = document.createElement('div'); |
03 | document.body.appendChild(container); |
05 | var info = document.createElement('div'); |
06 | info.style.position = 'absolute'; |
07 | info.style.top = '10px'; |
08 | info.style.width = '100%'; |
09 | info.style.textAlign = 'center'; |
11 | container.appendChild(info); |
13 | scene = new THREE.Scene(); |
15 | scene.fog = new THREE.Fog(0xcce0ff, 500, 10000); |
17 | camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000); |
18 | camera.position.set(0, 100, 500); |
21 | group = new THREE.Object3D(); |
Materials
Materials is one of the most interesting chapters in the graph. Especially when it comes to shiny surfaces and BumpMapping. Add the following code in the ‘init’ function:
02 | var imgTexture = THREE.ImageUtils.loadTexture('img/texture.jpg'); |
03 | imgTexture.repeat.set(1, 1); |
04 | imgTexture.wrapS = imgTexture.wrapT = THREE.RepeatWrapping; |
05 | imgTexture.anisotropy = 16; |
06 | imgTexture.needsUpdate = true; |
07 | var shininess = 50, specular = 0x333333, bumpScale = 1, shading = THREE.SmoothShading; |
09 | materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, bumpMap: imgTexture, bumpScale: bumpScale, color: 0xff0000, ambient: 0xffffff, specular: specular, shininess: shininess, shading: shading } ) ); |
10 | materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, color: 0x008800, ambient: 0xffffff, specular: specular, shininess: shininess, shading: shading } ) ); |
11 | materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, color: 0x584000, ambient: 0xffffff, shading: shading } ) ); |
12 | materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, color: 0xff0000, ambient: 0xffffff, shading: shading } ) ); |
Thus we have prepared four materials
Trunk (CylinderGeometry)
This is the first basic object on the scene: the truncated cylinder. Add the following code in the ‘init’ function:
2 | var trunk = new THREE.Mesh(new THREE.CylinderGeometry(2, 20, 300, 30, 1, false), materials[2]); |
We choosed the third material for the trunk. Params of the CylinderGeometry are: radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded.
Branches
In our example, the branch is a polygonal star. The upper row has the lowest number of teeth (branches), each subsequent row has one branch more. I prepared a special function ‘addBranch’ to generate the polygonal star. Add the following code in the ‘init’ function:
02 | function addBranch(count, x, y, z, opts, material, rotate) { |
05 | for (i = 0; i < count * 2; i++) { |
11 | var a = i / count * Math.PI; |
12 | points.push( new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l)); |
14 | var branchShape = new THREE.Shape(points); |
15 | var branchGeometry = new THREE.ExtrudeGeometry(branchShape, opts); |
16 | var branchMesh = new THREE.Mesh(branchGeometry, material); |
17 | branchMesh.position.set(x, y, z); |
20 | branchMesh.rotation.set(Math.PI / 2, 0, 0); |
22 | branchMesh.rotation.set(0, 0, Math.PI / 2); |
25 | group.add(branchMesh); |
Where ‘count’ is amount of branches at row; x, y, z – initial position, opts – options, and so on. The function generates the star mech object on the basis of calculated points in the figure.
Empirically I decided to display 14 rows of branches. Continue to add the code:
10 | for (i1 = 0; i1 < iBranchCnt; i1++) { |
11 | addBranch(iBranchCnt + 3 - i1, 0, -125 + i1*20, 0, options, materials[1], true); |
The star on the tree
The star on the tree is a star consisting of 5 rays. We may use the ready function ‘addBranch’ to show it:
6 | addBranch(5, 0, 160, -2, starOpts, materials[3], false); |
The only difference is that we have to turn this star 90% on the Z-axis
Christmas toys
Christmas toys are a good decoration of the Christmas tree. They need to be located on the branches of the tree. So I decided to slightly modify the function ‘addBranch’, by adding the code to add the balls. Now back to this function, find the row:
1 | points.push( new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l)); |
and add the following code below it:
1 | if (rotate && i % 2 == 0) { |
2 | var sphGeometry = new THREE.SphereGeometry(8); |
3 | sphMesh = new THREE.Mesh(sphGeometry, materials[0]); |
4 | sphMesh.position.set(Math.cos(a) * l*1.25, y, Math.sin(a) * l*1.25); |
All balls are spheres (SphereGeometry) with radius of 8.
Ground
Next step is to add the earth, for which we will use a different texture. As the surface (ground) we will use PlaneGeometry. Add the following code right after the place you finished adding the Star:
02 | var groundColor = new THREE.Color(0xd2ddef); |
03 | var groundTexture = THREE.ImageUtils.generateDataTexture(1, 1, groundColor); |
04 | var groundMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0x111111, map: groundTexture }); |
05 | var groundTexture = THREE.ImageUtils.loadTexture('img/ground.jpg', undefined, function() { groundMaterial.map = groundTexture }); |
06 | groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping; |
07 | groundTexture.repeat.set(25, 25); |
08 | groundTexture.anisotropy = 16; |
09 | var groundMesh = new THREE.Mesh( new THREE.PlaneGeometry(20000, 20000), groundMaterial); |
10 | groundMesh.position.y = -150; |
11 | groundMesh.rotation.x = - Math.PI / 2; |
12 | group.add(groundMesh); |
Snowflakes
Another important element – snowflakes. They represent a Particle System. Continue to add the code:
03 | var sfTexture = THREE.ImageUtils.loadTexture('img/snowflake.png'); |
04 | var sfGeometry = new THREE.Geometry(); |
05 | for (i = 0; i < 10000; i++) { |
06 | var vertex = new THREE.Vector3(); |
07 | vertex.x = Math.random() * 2000 - 1000; |
08 | vertex.y = Math.random() * 2000 - 1000; |
09 | vertex.z = Math.random() * 2000 - 1000; |
10 | sfGeometry.vertices.push(vertex); |
12 | var states = [ [ [1.0, 0.2, 0.9], sfTexture, 10 ], [ [0.90, 0.1, 0.5], sfTexture, 8 ], [ [0.80, 0.05, 0.5], sfTexture, 5 ] ]; |
13 | for (i = 0; i < states.length; i++) { |
15 | sprite = states[i][1]; |
17 | sfMats[i] = new THREE.ParticleSystemMaterial({ size: size, map: sprite, blending: THREE.AdditiveBlending, depthTest: false, transparent : true }); |
18 | sfMats[i].color.setHSL(color[0], color[1], color[2]); |
19 | particles = new THREE.ParticleSystem(sfGeometry, sfMats[i]); |
20 | particles.rotation.x = Math.random() * 10; |
21 | particles.rotation.y = Math.random() * 10; |
22 | particles.rotation.z = Math.random() * 10; |
Light
Basically – everything is ready and we can start adding the last element of the scene – light. Light sources will be a few: AmbientLight, the flying white PointLight and the secondary blue DirectionalLight. Add the following code:
03 | scene.add( new THREE.AmbientLight(0x222222)); |
05 | particleLight = new THREE.Mesh( new THREE.SphereGeometry(5, 10, 10), new THREE.MeshBasicMaterial({ color: 0xffffff })); |
06 | particleLight.position.y = 250; |
07 | group.add(particleLight); |
09 | pointLight = new THREE.PointLight(0xffffff, 1, 1000); |
10 | group.add(pointLight); |
11 | pointLight.position = particleLight.position; |
13 | var directionalLight = new THREE.DirectionalLight(0x0000ff, 2); |
14 | directionalLight.position.set(10, 1, 1).normalize(); |
15 | group.add(directionalLight); |
Finishing touches
Let’s add the latest lines of code in the ‘init’ function to create the render object (WebGLRenderer), and adjust it’s properties. We also need to add several UI event handlers. Add the following code:
02 | renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); |
03 | renderer.setClearColor(scene.fog.color); |
04 | renderer.setSize(window.innerWidth, window.innerHeight); |
05 | container.appendChild(renderer.domElement); |
06 | renderer.gammaInput = true; |
07 | renderer.gammaOutput = true; |
08 | renderer.physicallyBasedShading = true; |
10 | document.addEventListener('mousedown', onDocumentMouseDown, false); |
11 | document.addEventListener('touchstart', onDocumentTouchStart, false); |
12 | document.addEventListener('touchmove', onDocumentTouchMove, false); |
13 | window.addEventListener('resize', onWindowResize, false); |
Event handlers functions
Here all the features added in response to the events of the user interface. You need to add this code immediately after the ‘init’ function:
01 | function onWindowResize() { |
02 | windowHalfX = window.innerWidth / 2; |
03 | windowHalfY = window.innerHeight / 2; |
04 | camera.aspect = window.innerWidth / window.innerHeight; |
05 | camera.updateProjectionMatrix(); |
06 | renderer.setSize( window.innerWidth - 20, window.innerHeight - 20 ); |
08 | function onDocumentMouseDown(event) { |
09 | event.preventDefault(); |
10 | document.addEventListener('mousemove', onDocumentMouseMove, false); |
11 | document.addEventListener('mouseup', onDocumentMouseUp, false); |
12 | document.addEventListener('mouseout', onDocumentMouseOut, false); |
13 | mouseXOnMouseDown = event.clientX - windowHalfX; |
14 | targetRotationOnMouseDown = targetRotation; |
16 | function onDocumentMouseMove(event) { |
17 | mouseX = event.clientX - windowHalfX; |
18 | targetRotation = targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02; |
20 | function onDocumentMouseUp(event) { |
21 | document.removeEventListener('mousemove', onDocumentMouseMove, false); |
22 | document.removeEventListener('mouseup', onDocumentMouseUp, false); |
23 | document.removeEventListener('mouseout', onDocumentMouseOut, false); |
25 | function onDocumentMouseOut(event) { |
26 | document.removeEventListener('mousemove', onDocumentMouseMove, false); |
27 | document.removeEventListener('mouseup', onDocumentMouseUp, false); |
28 | document.removeEventListener('mouseout', onDocumentMouseOut, false); |
30 | function onDocumentTouchStart(event) { |
31 | if (event.touches.length == 1) { |
32 | event.preventDefault(); |
33 | mouseXOnMouseDown = event.touches[0].pageX - windowHalfX; |
34 | targetRotationOnMouseDown = targetRotation; |
37 | function onDocumentTouchMove(event) { |
38 | if (event.touches.length == 1) { |
39 | event.preventDefault(); |
40 | mouseX = event.touches[0].pageX - windowHalfX; |
41 | targetRotation = targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02; |
Render
Finally, add a little more interactivity: making rotate the scene, move the source of light, and the camera zoom in and out of the scene. We need to modify the ready ‘render’ function:
02 | var timer = Date.now() * 0.00025; |
03 | group.rotation.y += (targetRotation - group.rotation.y) * 0.01; |
04 | particleLight.position.x = Math.sin(timer * 7) * 300; |
05 | particleLight.position.z = Math.cos(timer * 3) * 300; |
06 | camera.position.x = Math.cos(timer) * 1000; |
07 | camera.position.z = Math.sin(timer) * 500; |
08 | camera.lookAt(scene.position); |
09 | renderer.render(scene, camera); |
[sociallocker]
[/sociallocker]
Conclusion
I think that we have built a great Christmas card today. I hope we have not wasted time and you like the result. Merry Christmas and Happy New Year.
PS: I also wanted to report that was recently completed a redesign of our website, it is now fully responsive and works on mobile devices. This year we also launched a new section of web languages references. Also recently launched a new section of premium resources. It contains the most interesting scripts in the boxed version. If you either want to buy something, or even if you just want to make a donation – you are welcome.