Christmas tree with three.js

Christmas tree with three.js

1 316
Christmas tree with three.js
Christmas tree with three.js

Christmas tree with three.js

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:

Live 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:

<!DOCTYPE html>
<html lang="en" >
    <head>
        <meta charset="utf-8" />
        <meta name="author" content="Script Tutorials" />
        <title>Christmas tree with three.js | Script Tutorials</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

        <!-- add styles -->
        <link href="css/main.css" rel="stylesheet" type="text/css" />
    </head>
    <body>
        <!-- add scripts -->
        <script src="js/three.min.js"></script>
        <script src="js/script.js"></script>
    </body>
</html>

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

var container;
var camera, scene, renderer, group;
var targetRotation = 0;
var targetRotationOnMouseDown = 0;
var mouseX = 0;
var mouseXOnMouseDown = 0;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;

// initialize and start animation
init();
animate();

function init() {

}

function animate() {
    requestAnimationFrame(animate);

    render();
}

function render() {

    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:

    // prepare the container
    container = document.createElement('div');
    document.body.appendChild(container);

    // display Info
    var info = document.createElement('div');
    info.style.position = 'absolute';
    info.style.top = '10px';
    info.style.width = '100%';
    info.style.textAlign = 'center';
    info.innerHTML = 'This Christmas tree is prepared by <a target="_blank" href="http://www.script-tutorials.com/">Script Tutorials</a> team.<br/>Drag to spin';
    container.appendChild(info);

    // initialize the scene
    scene = new THREE.Scene();

    // add the fog
    scene.fog = new THREE.Fog(0xcce0ff, 500, 10000);

    // set the camera
    camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000);
    camera.position.set(0, 100, 500);
    scene.add(camera);

    // create the empty scene group
    group = new THREE.Object3D();
    scene.add(group);

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:

    // prepare materials
    var imgTexture = THREE.ImageUtils.loadTexture('img/texture.jpg');
    imgTexture.repeat.set(1, 1);
    imgTexture.wrapS = imgTexture.wrapT = THREE.RepeatWrapping;
    imgTexture.anisotropy = 16;
    imgTexture.needsUpdate = true;

    var shininess = 50, specular = 0x333333, bumpScale = 1, shading = THREE.SmoothShading;

    var materials = [];
    materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, bumpMap: imgTexture, bumpScale: bumpScale, color: 0xff0000, ambient: 0xffffff, specular: specular, shininess: shininess, shading: shading } ) );
    materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, color: 0x008800, ambient: 0xffffff, specular: specular, shininess: shininess, shading: shading } ) );
    materials.push( new THREE.MeshPhongMaterial( { map: imgTexture, color: 0x584000, ambient: 0xffffff, shading: shading } ) );
    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:

    // add the Trunk
    var trunk = new THREE.Mesh(new THREE.CylinderGeometry(2, 20, 300, 30, 1, false), materials[2]);
    group.add(trunk);

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:

    // add branch function
    function addBranch(count, x, y, z, opts, material, rotate) {

        // prepare star-like points
        var points = [], l;
        for (i = 0; i < count * 2; i++) {
            if (i % 2 == 1) {
                l = count * 2;
            } else {
                l = count * 4;
            }
            var a = i / count * Math.PI;
            points.push( new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l));
        }

        var branchShape = new THREE.Shape(points);
        var branchGeometry = new THREE.ExtrudeGeometry(branchShape, opts);
        var branchMesh = new THREE.Mesh(branchGeometry, material);

        branchMesh.position.set(x, y, z);

        // rotate 90 degrees
        if (rotate) {
            branchMesh.rotation.set(Math.PI / 2, 0, 0);
        } else {
            branchMesh.rotation.set(0, 0, Math.PI / 2);
        }

        // add branch to the group
        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:

    // options
    var options = {
        amount: 6,
        bevelEnabled: true,
        bevelSegments: 5,
        steps: 2
    };

    // add 10 branches
    var iBranchCnt = 14;
    for (i1 = 0; i1 < iBranchCnt; i1++) {
        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:

    // add the star
    var starOpts = {
        amount: 4,
        bevelEnabled: false
    };
    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:

points.push( new THREE.Vector2(Math.cos(a) * l, Math.sin(a) * l));

and add the following code below it:

    if (rotate && i % 2 == 0) {
        var sphGeometry = new THREE.SphereGeometry(8);
        sphMesh = new THREE.Mesh(sphGeometry, materials[0]);
        sphMesh.position.set(Math.cos(a) * l*1.25, y, Math.sin(a) * l*1.25);
        group.add(sphMesh);
    }

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:

    // add the ground
    var groundColor = new THREE.Color(0xd2ddef);
    var groundTexture = THREE.ImageUtils.generateDataTexture(1, 1, groundColor);
    var groundMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff, specular: 0x111111, map: groundTexture });

    var groundTexture = THREE.ImageUtils.loadTexture('img/ground.jpg', undefined, function() { groundMaterial.map = groundTexture });
    groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
    groundTexture.repeat.set(25, 25);
    groundTexture.anisotropy = 16;

    var groundMesh = new THREE.Mesh( new THREE.PlaneGeometry(20000, 20000), groundMaterial);
    groundMesh.position.y = -150;
    groundMesh.rotation.x = - Math.PI / 2;
    group.add(groundMesh);

Snowflakes

Another important element – snowflakes. They represent a Particle System. Continue to add the code:

    // add snowflakes
    var sfMats = [];
    var sfTexture = THREE.ImageUtils.loadTexture('img/snowflake.png');
    var sfGeometry = new THREE.Geometry();
    for (i = 0; i < 10000; i++) {
        var vertex = new THREE.Vector3();
        vertex.x = Math.random() * 2000 - 1000;
        vertex.y = Math.random() * 2000 - 1000;
        vertex.z = Math.random() * 2000 - 1000;

        sfGeometry.vertices.push(vertex);
    }

    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 ] ];
    for (i = 0; i < states.length; i++) {
        color  = states[i][0];
        sprite = states[i][1];
        size   = states[i][2];

        sfMats[i] = new THREE.ParticleSystemMaterial({ size: size, map: sprite, blending: THREE.AdditiveBlending, depthTest: false, transparent : true });
        sfMats[i].color.setHSL(color[0], color[1], color[2]);

        particles = new THREE.ParticleSystem(sfGeometry, sfMats[i]);

        particles.rotation.x = Math.random() * 10;
        particles.rotation.y = Math.random() * 10;
        particles.rotation.z = Math.random() * 10;

        group.add(particles);
    }

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:

    // Add lights:

    // add ambient (global) light
    scene.add( new THREE.AmbientLight(0x222222));

    // add particle of light
    particleLight = new THREE.Mesh( new THREE.SphereGeometry(5, 10, 10), new THREE.MeshBasicMaterial({ color: 0xffffff }));
    particleLight.position.y = 250;
    group.add(particleLight);

    // add flying pint light
    pointLight = new THREE.PointLight(0xffffff, 1, 1000);
    group.add(pointLight);

    pointLight.position = particleLight.position;

    // add directional blue light
    var directionalLight = new THREE.DirectionalLight(0x0000ff, 2);
    directionalLight.position.set(10, 1, 1).normalize();
    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:

    // prepare the render object and render the scene
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    renderer.setClearColor(scene.fog.color);

    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    renderer.gammaInput = true;
    renderer.gammaOutput = true;
    renderer.physicallyBasedShading = true;

    // add events handlers
    document.addEventListener('mousedown', onDocumentMouseDown, false);
    document.addEventListener('touchstart', onDocumentTouchStart, false);
    document.addEventListener('touchmove', onDocumentTouchMove, false);
    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:

function onWindowResize() {

    windowHalfX = window.innerWidth / 2;
    windowHalfY = window.innerHeight / 2;

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

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

function onDocumentMouseDown(event) {
    event.preventDefault();

    document.addEventListener('mousemove', onDocumentMouseMove, false);
    document.addEventListener('mouseup', onDocumentMouseUp, false);
    document.addEventListener('mouseout', onDocumentMouseOut, false);

    mouseXOnMouseDown = event.clientX - windowHalfX;
    targetRotationOnMouseDown = targetRotation;
}

function onDocumentMouseMove(event) {
    mouseX = event.clientX - windowHalfX;
    targetRotation = targetRotationOnMouseDown + (mouseX - mouseXOnMouseDown) * 0.02;
}

function onDocumentMouseUp(event) {
    document.removeEventListener('mousemove', onDocumentMouseMove, false);
    document.removeEventListener('mouseup', onDocumentMouseUp, false);
    document.removeEventListener('mouseout', onDocumentMouseOut, false);
}

function onDocumentMouseOut(event) {
    document.removeEventListener('mousemove', onDocumentMouseMove, false);
    document.removeEventListener('mouseup', onDocumentMouseUp, false);
    document.removeEventListener('mouseout', onDocumentMouseOut, false);
}

function onDocumentTouchStart(event) {
    if (event.touches.length == 1) {
        event.preventDefault();

        mouseXOnMouseDown = event.touches[0].pageX - windowHalfX;
        targetRotationOnMouseDown = targetRotation;
    }
}

function onDocumentTouchMove(event) {
    if (event.touches.length == 1) {
        event.preventDefault();

        mouseX = event.touches[0].pageX - windowHalfX;
        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:

function render() {
    var timer = Date.now() * 0.00025;

    group.rotation.y += (targetRotation - group.rotation.y) * 0.01;

    particleLight.position.x = Math.sin(timer * 7) * 300;
    particleLight.position.z = Math.cos(timer * 3) * 300;

    camera.position.x = Math.cos(timer) * 1000;
    camera.position.z = Math.sin(timer) * 500;
    camera.lookAt(scene.position);

    renderer.render(scene, camera);
}

Live Demo

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.

SIMILAR ARTICLES


1 COMMENT

Leave a Reply