Per Brage's Blog

const String ABOUT = "Somehow related to code";

Tag Archives: Canvas

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