Per Brage's Blog

const String ABOUT = "Somehow related to code";

Tag Archives: HTML5

HTML5/JavaScript Cube (Part 2: Improving with Three.js)

In part one I showed you how to create a spinning cube by building a mesh, and then applying basic linear algebra to make it rotate. The development was fun in itself but looking at the surrounding world, and what happened the past 15 years or so, evolution definitely leaped forward. These days there are tons of 3D engines, frameworks, libraries and what not, so there is really no need to do all that work yourself. As it happens to be, there is this JavaScript 3D library named Three.js, which enables you to create stunning 3D graphics with a small amount of effort. I thought it would be fun to see if I could improve my cube by refactoring it to use Three.js, and at the same time add some new features like texture mapping and light sources.

However, when adding features like texture mapping there is a rather large issue with browser compatibility that presents itself, which I think requires a bit of explanation. Three.js has three different main renderers; CanvasRenderer, SVGRenderer and WebGLRenderer (with some variations of WebGLRenderers). They all have a few pros and cons, but I’m focusing on the problems related to my features.

  • CanvasRenderer
  • Works on all major browsers since its just a HTML5 Canvas and JavaScript. The bad part is that the texture mapping routine is not perspective correct, and there is no interpolation in the triangle drawing routine, which produces gaps between aligned polygons. And when I tested it, I couldn’t even get the texture mapping to work in IE9.

  • SVGRenderer
  • Doesn’t support texture mapping at all, so that is a no go, at least for the new features I want to add.

  • WebGLRenderer
  • The best of all three renderers, but the problem is that WebGL is not supported by all major browsers, especially IE. It seems that people are still using IE for some unknown reasons, but in my opinion they should just remove the 6 out of http://www.ie6countdown.com/. Even so, I’m not sure IE will ever support WebGL since it’s based on OpenGL, which is a direct competitor to Microsoft DirectX. Microsoft also openly stated that WebGL is insecure, as it directly exposes hardware functionality to the web. I guess they may have a point there!

This creates a problem with Three.js, which is that there is not a single solution that allows me to texture map a cube and have it work on all major browsers (speaking of recent versions). For the sake of this post and the challenge I started, I will use WebGLRenderer and strictly require Chrome and Firefox browsers to use it. So if you can’t click and see the demo below, switch to a real browser.

Refactoring to use Three.js

In my new cube I have reduced the lines of code from about 250 lines, to 90 lines of code (not minified JavaScript). And the features I wanted to add, texture mapping and light sources was of course provided by Three.js out of the box. The code, when refactored turned into a few simple steps; create renderer and a scene, add a camera, a cube and then just add our light sources to the scene. Still, I have to take care of the actual spinning myself, but that can now be done without any linear algebra.

Here are the few very simple functions that replaces most of my code from part one.


var buildCamera = function () {
    var camera = new THREE.PerspectiveCamera(50, viewPortWidth / viewPortHeight, 1, 1000);
    camera.position.z = 500;
    return camera;
};

var buildCube = function () {
    var cubeGeometry = new THREE.CubeGeometry(200, 200, 200, 1, 1, 1, buildMaterials());
    return new THREE.Mesh(cubeGeometry, new THREE.MeshFaceMaterial());
};

var buildDirectionalLight = function () {
    var directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(1, 1, 1).normalize();
    return directionalLight;
};

var buildAmbientLight = function () {
    return new THREE.AmbientLight(0x999999);
};

The refresh function is similar as before, but Three.js takes care of the drawing now. So the refresh function was refactored into a calculateCube function that increases the angle, and then sets the angle on the cubes rotation properties before it renders the scene.


var calculateCube = function (cube, scene, camera) {
    angle++;
    cube.rotation.x = degreesToRadians(angle);
    cube.rotation.y = degreesToRadians(angle);
    cube.rotation.z = degreesToRadians(angle);
    renderer.render(scene, camera);
};

Now we simply add the output of each and every function above to the scene, and then start a timer to call our calculateCube function.

        var scene = new THREE.Scene();
        var camera = buildCamera();
        var cube = buildCube();

        scene.add(camera);
        scene.add(cube);
        scene.add(buildAmbientLight());
        scene.add(buildDirectionalLight());

        setInterval(function () {
            calculateCube(cube, scene, camera);
        }, 20);

Result

threejscubeResult

Links

Live demo of spinning cube using Three.js
Full source at my blog-examples repository on GitHub

Demonstrations of the Three.js library.

HTML5/JavaScript Cube (Part 1: Applying old school)

In my first post I showed a picture of a custom design that contains a spinning cube in the upper left corner. That cube wasn’t supposed to end up in the design since it originally was an endeavor I started to try out the HTML5 canvas. I figured I would do that through applying some old 3D programming knowledge I learned in the early nineties while me and a couple of friends did a lot of 3D programming (or at least was trying to). For reasons I don’t even remember today, I didn’t have time to dig further into the abyss of 3D programming and the high point of my 3D programming career ended with a gouraud shaded dolphin spinning in an X11 window. I did attempt to create a 3D world with a plane flying through it, but for reason already stated I never got around to get it working.

This time around I only wanted to create a basic flat shaded cube! I know, it’s 2012 and how sexy is a spinning cube these days, really? Even so, I wanted to take some time blogging about two ways of accomplishing the same task and I will try to keep this first post as simple as possible! So here is part one of creating a spinning cube using HTML5/JavaScript.

Creating the mesh

First of all we need to construct the cube, and to create the cube we need to create some vertices, connect the vertices to create polygons and then finally add them to a mesh. These three concepts is what’s needed to model a simple 3D object, so I started out with creating a small mesh creator that produces vertices, polygons and finally a cube mesh!

Vertices

A vertex represents a coordinate in 3D space, and is a construct of three values X, Y and Z. These three values specify how the vertex relates to the center of the object we are modeling. I made a small function that produces a Json object representing a vertex which contains both the initial values, the values we use at design time and base our calculations on, and the current values which are the calculated values used during drawing.

    var createVertex = function (x, y, z) {
        return {
            "initialX": x,
            "initialY": y,
            "initialZ": z,
            "currentX": 0,
            "currentY": 0,
            "currentZ": 0
        };
    };

Polygons

Polygons are flat geometry shapes consisting of straight lines that are joined to form a circuit. Each corner of the polygon is defined by a vertex. For example, to form a triangle polygon we need to have 3 vertices, one for each corner of the triangle. The same vertex can however take part in multiple polygons.

polygonExamples

Polygons come in various forms and shapes, but for 3D programming we always use triangles otherwise we get into trouble on more advanced topics. However, for this cube I am using square polygons for the single reason that I didn’t want to develop my own interpolating triangle method to avoid the gap between aligned polygons. (I’m actually surprised no browser actually solves this issue built-in). Anyways, this small function is actually all we need to produce a square polygon.

    var createPolygon = function (vA, vB, vC, vD) {
        return {
            "vertices": [vA, vB, vC, vD],
            "averageZ": null
        };
    };

The averageZ property is something I will use later on for both shading and polygon sorting.

Mesh

A mesh is the actual 3D object we are modeling, and it consists of all the vertices and polygons in such a way that it looks like an object, in our case, a cube.

    var createCubeMesh = function () {
        var mesh = { "polygons": null };
        var size = 70;

        var vertices = [];
        vertices.push(createVertex(size, size, size));
        vertices.push(createVertex(size, -size, size));
        vertices.push(createVertex(-size, size, size));
        vertices.push(createVertex(-size, -size, size));
        vertices.push(createVertex(size, size, -size));
        vertices.push(createVertex(size, -size, -size));
        vertices.push(createVertex(-size, size, -size));
        vertices.push(createVertex(-size, -size, -size));

        var polygons = mesh.polygons = [];
        polygons.push(createPolygon(vertices[0], vertices[2], vertices[3], vertices[1]));
        polygons.push(createPolygon(vertices[0], vertices[2], vertices[6], vertices[4]));
        polygons.push(createPolygon(vertices[0], vertices[1], vertices[5], vertices[4]));
        polygons.push(createPolygon(vertices[1], vertices[3], vertices[7], vertices[5]));
        polygons.push(createPolygon(vertices[3], vertices[2], vertices[6], vertices[7]));
        polygons.push(createPolygon(vertices[4], vertices[6], vertices[7], vertices[5]));

        return mesh;
    };

Right now we can actually get a cube drawn on our canvas since we can just omit the Z value of each vertex and draw each polygon of the mesh. We wouldn’t really see a 3D cube though, but rather a square as we would only see one side of the cube. (You’re right, this does depend on the initial values in each vertex, and it could have been designed from another angle.)

Adding rotation

Rotation is were we need to start using some math, and we need to use linear algebra to apply rotation to each vertex. Now, I’m definitely not an expert in linear algebra and won’t go into the depths of the topic. But if you want to dig deeper, I suggest you Google it! I’m quite sure you will find good information among the almost 10 million hits you will get!

To get our cube to rotate, we basically need to create one matrix for each rotation around an axis, that means one for each of the X-axis, Y-axis and Z-axis. Each of these matrixes will be calculated based on an identity matrix with the degrees (converted to radians) of rotation we wish to apply.

When all three rotation matrixes have been created we need to multiply them together to get a single matrix that represents all three rotations. We can then use this combined matrix to calculate each and every vertex and apply our rotation to them. For this we need two functions, first one to multiply matrixes, and then another one to apply a matrix to a vertex.

    var multiplyMatrixes = function (matrix1, matrix2) {
        var matrix = createIdentityMatrix();
        for (var i = 0; i < 3; i++) {
            for (var j = 0; j < 3; j++) {
                matrix[i][j] =
                    (matrix2[i][0] * matrix1[0][j]) +
                        (matrix2[i][1] * matrix1[1][j]) +
                            (matrix2[i][2] * matrix1[2][j]);
            }
        }
        return matrix;
    };
    var applyMatrixToVertex = function (matrix, vertex) {
        vertex.currentX = (vertex.initialX * matrix[0][0]) + 
                          (vertex.initialY * matrix[0][1]) + 
                          (vertex.initialZ * matrix[0][2]);

        vertex.currentY = (vertex.initialX * matrix[1][0]) + 
                          (vertex.initialY * matrix[1][1]) + 
                          (vertex.initialZ * matrix[1][2]);

        vertex.currentZ = (vertex.initialX * matrix[2][0]) + 
                          (vertex.initialY * matrix[2][1]) + 
                          (vertex.initialZ * matrix[2][2]);

        return vertex;
    };

Some final calculations

Applying perspective

After we have calculated the rotation using matrixes we need to apply some perspective to our cube, that is the further away a vertex, polygon or mesh is, the smaller it should look. Think of a long train before you get hit by it, the locomotive looks pretty huge while the last wagon is pretty small, almost like you could squash it between your fingers (that is if you are fast enough before YOU get squashed). So applying perspective is basically that, as Z gets larger, distance is increasing and the object should look smaller. There are various ways of applying perspective but this simple z-divide function does the trick well enough.

    var applyPerspective = function (vertex) {
        vertex.currentX = vertex.currentX * perspectiveCoefficient / 
                          (vertex.currentZ + perspectiveCoefficient);
        vertex.currentY = vertex.currentY * perspectiveCoefficient / 
                          (vertex.currentZ + perspectiveCoefficient);
    };

Z-Ordering

When drawing the polygons on the canvas we can’t just draw them in the order they were modeled, we need to draw them in the order of their location on the Z-axis, from back to front. A real 3D engine would not even draw those in the back that are covered by polygons in front of them, or polygons that are facing away from the camera, but by simply sorting the polygons we can achieve the same result without complex calculations.

First we need to calculate the Z average of each polygon,

    var calculateZAverage = function (polygon) {
        var zSum = 0;
        for (var i = 0; i < polygon.vertices.length; i++) {
            zSum += polygon.vertices[i].currentZ;
        }
        polygon.averageZ = zSum / polygon.vertices.length;
    };

And then we simply sort the polygons,

    var sortPolygons = function (polygons) {
        return polygons.sort(function (polygon1, polygon2) {
            return polygon2.averageZ - polygon1.averageZ;
        });
    };

Flat shading

Using this average Z value of each polygon we can now also apply flat shading, meaning that depending on a polygons average Z we can apply a color between 0 and 255, which mean from black to white in this example. This type of shading is also a trick to avoid introducing light sources into our scene, which would require us to calculate polygon normals and the distances from light sources, and its directions etc.

Drawing

We have finally reached the stage where it’s time to draw the polygons on the canvas, which should result in a cube rotating on our screen. After initialization we add a timer calling a refresh function which basically does 4 things.

  1. Increase the angle
  2. Calculate the rotation, perspective and z-order
  3. Clear the current canvas
  4. Draw the cube

The actual drawing functions of the mesh and the polygon looks like this.

    var drawMesh = function () {
        sortPolygons(mesh.polygons);
        for (var k = 0; k < mesh.polygons.length; k++) {
            drawPolygon(mesh.polygons[k]);
        }
        ctx.restore();
    };
    var drawPolygon = function (polygon) {
        var shade = calculateShade(polygon);
        ctx.beginPath();
        ctx.fillStyle = 'rgb(' + shade + ',' + shade + ',' + shade + ')';
        ctx.moveTo(polygon.vertices[0].currentX, polygon.vertices[0].currentY);
        for (var i = 1; i < polygon.vertices.length; i++) {
            ctx.lineTo(polygon.vertices[i].currentX, polygon.vertices[i].currentY);
        }
        ctx.fill();
        ctx.closePath();
    };

Result

Here is a picture of the result! Produced by about 250 lines of JavaScript!

cubeResult

Links