WebGL Essentials: Part II

WebGL Essentials: Part II

This entry is part 2 of 3 in the series WebGL Essentials

The Full Series

This article will build on to the framework introduced in part one of this mini-series, adding a model importer and a custom class for 3D objects. You will also be introduced to animation and controls. There’s a lot to go through, so let’s get started!

This article relies heavily on the first article, so, if you haven’t read it yet, you should start there first.

The way WebGL manipulates items in the 3D world is by using math formulas known as transformations. So, before we start building the 3D class, I will show you some of the different kinds of transformations and how they are implemented.


Transformations

There are three basic transformations when working with 3D objects.

  • Moving
  • Scaling
  • Rotating

Each of these functions can be performed on either the X, Y, or Z axis, making a total possibility of nine basic transformations. All of these affect the 3D object’s 4×4 transformation matrix in different ways. In order to perform multiple transformations on the same object without overlapping problems, we have to multiply the transformation into the object’s matrix and not apply it to the object’s matrix directly. Moving is the easiest to do, so let’s start there.

Moving A.K.A. “Translation”

Moving a 3D object is one of the easiest transformations you can do, because there is a special place in the 4×4 matrix for it. There’s no need for any math; just put the X, Y and Z coordinates in the matrix and your done. If you are looking at the 4×4 matrix, then it’s the first three numbers in the bottom row. Additionally, you should know that positive Z is behind the camera. Therefore, a Z value of -100 places the object 100 units inwards on the screen. We will compensate for this in our code.

In order to perform multiple transformations, you can’t simply change the object’s real matrix; you must apply the transformation to a new blank matrix, known as an identity matrix, and multiply it with the main matrix.

Matrix multiplication can be a bit tricky to understand, but the basic idea is that each vertical column is multiplied by the second matrix’s horizontal row. For example, the first number would be the first row multiplied by the other matrix’s first column. The second number in the new matrix would be the first row multiplied by the other matrix’s second column, and so on.

The following snippet is code I wrote for multiplying two matrices in JavaScript. Add this to your .js file that you made in the first part of this series:

function MH(A, B) {
    var Sum = 0;
    for (var i = 0; i < A.length; i++) {
        Sum += A[i] * B[i];
    }
    return Sum;
}

function MultiplyMatrix(A, B) {
    var A1 = [A[0], A[1], A[2], A[3]];
    var A2 = [A[4], A[5], A[6], A[7]];
    var A3 = [A[8], A[9], A[10], A[11]];
    var A4 = [A[12], A[13], A[14], A[15]];

    var B1 = [B[0], B[4], B[8], B[12]];
    var B2 = [B[1], B[5], B[9], B[13]];
    var B3 = [B[2], B[6], B[10], B[14]];
    var B4 = [B[3], B[7], B[11], B[15]];

    return [
    MH(A1, B1), MH(A1, B2), MH(A1, B3), MH(A1, B4),
    MH(A2, B1), MH(A2, B2), MH(A2, B3), MH(A2, B4),
    MH(A3, B1), MH(A3, B2), MH(A3, B3), MH(A3, B4),
    MH(A4, B1), MH(A4, B2), MH(A4, B3), MH(A4, B4)];
}

I don’t think this requires any explanation, as it’s just the necessary math for matrix multiplication. Let’s move on to scaling.

Scaling

Scaling a model is also fairly simple – it’s simple multiplication. You have to multiply the first three diagonal numbers by whatever the scale is. Once again, the order is X, Y, and Z. So, if you want to scale your object to be two times bigger in all three axes, you would multiply the first, sixth, and eleventh elements in your array by 2.

Rotating

Rotating is the trickiest transformation because there is a different equation for each of the three axis. The following image shows the rotation equations for each axis:

Don’t worry if this picture doesn’t make sense to you; we’ll review the JavaScript implementation soon.

It’s important to note that it matters what order you perform the transformations; different orders produce different results.

It’s important to note that it matters what order you perform the transformations; different orders produce different results. If you first move your object and then rotate it, WebGL will swing your object around like a bat, as opposed to rotating the object in place. If you rotate first and then move your object, you will have an object in the specified location, but it will face the direction that you entered. This is because the transformations are performed around the origin point – 0,0,0 – in the 3D world. There is no right or wrong order. It all depends on the effect you are looking for.

It could require more than one of each transformations to make some advanced animations. For instance if you want a door to swing open on its hinges, you would move the door so that its hinges are on the Y axis (ie 0 on both the X and Z axis). You would then rotate on the Y axis so the door will swing on its hinges. Finally, you would move it again to the desired location in your scene.

These type of animations are a bit more custom-made for each situation, so I’m not going to make a function for it. I will, however, make a function with the most basic order which is: scaling, rotating, and then moving. This insures everything is in the specified location and facing the right way.

Now that you have a basic understanding of the math behind all of this and how animations work, let’s create a JavaScript data type to hold our 3D objects.


GL Objects

Remember from the first part of this series that you need three arrays in order to draw a basic 3D object: the vertices array, the triangles array, and the textures array. That will be the base of our data type. We also need variables for the three transformations on each of the three axes. Finally, we need a variables for the texture image and to indicate whether the model has finished loading.

Here is my implementation of a 3D object in JavaScript:

function GLObject(VertexArr, TriangleArr, TextureArr, ImageSrc) {
    this.Pos = {
        X: 0,
        Y: 0,
        Z: 0
    };
    this.Scale = {
        X: 1.0,
        Y: 1.0,
        Z: 1.0
    };
    this.Rotation = {
        X: 0,
        Y: 0,
        Z: 0
    };
    this.Vertices = VertexArr;
    this.Triangles = TriangleArr;
    this.TriangleCount = TriangleArr.length;
    this.TextureMap = TextureArr;
    this.Image = new Image();
    this.Image.onload = function () {
        this.ReadyState = true;
    };
    this.Image.src = ImageSrc;
    this.Ready = false;
    //Add Transformation function Here
}

I’ve added two separate “ready” variables: one for when the image is ready, and one for the model. When the image is ready, I will prepare the model by converting the image into a WebGL texture and buffer the three arrays into WebGL buffers. This will speed up our application, as apposed to buffering the data in every draw cycle. Since we will convert the arrays into buffers, we need to save the number of triangles in a separate variable.

Now, let’s add the function that will calculate the object’s transformation matrix. This function will take all the local variables and multiply them in the order that I mentioned earlier (scale, rotation, and then translation). You can play around with this order for different effects. Replace the //Add Transformation function Here comment with the following code:

this.GetTransforms = function () {
    //Create a Blank Identity Matrix
    var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];

    //Scaling
    var Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    Temp[0] *= this.Scale.X;
    Temp[5] *= this.Scale.Y;
    Temp[10] *= this.Scale.Z;
    TMatrix = MultiplyMatrix(TMatrix, Temp);

    //Rotating X
    Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    var X = this.Rotation.X * (Math.PI / 180.0);
    Temp[5] = Math.cos(X);
    Temp[6] = Math.sin(X);
    Temp[9] = -1 * Math.sin(X);
    Temp[10] = Math.cos(X);
    TMatrix = MultiplyMatrix(TMatrix, Temp);
    //Rotating Y
    Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    var Y = this.Rotation.Y * (Math.PI / 180.0);
    Temp[0] = Math.cos(Y);
    Temp[2] = -1 * Math.sin(Y);
    Temp[8] = Math.sin(Y);
    Temp[10] = Math.cos(Y);
    TMatrix = MultiplyMatrix(TMatrix, Temp);

    //Rotating Z
    Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    var Z = this.Rotation.Z * (Math.PI / 180.0);
    Temp[0] = Math.cos(Z);
    Temp[1] = Math.sin(Z);
    Temp[4] = -1 * Math.sin(Z);
    Temp[5] = Math.cos(Z);
    TMatrix = MultiplyMatrix(TMatrix, Temp);
    //Moving
    Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    Temp[12] = this.Pos.X;
    Temp[13] = this.Pos.Y;
    Temp[14] = this.Pos.Z * -1;

    return MultiplyMatrix(TMatrix, Temp);
}

Because the rotation formulas overlap one another, they have to be performed one at a time. This function replaces the MakeTransform function from the last tutorial, so you can remove it from your script.


OBJ Importer

Now that we have our 3D class built, we need a way to load the data. We’ll make a simple model importer that will convert .obj files into the necessary data to make one of our newly created GLObject objects. I am using the .obj model format because it stores all the data in a raw form, and it has very good documentation on how it stores the information. If your 3D modeling program doesn't support exporting to .obj, then you can always create an importer for some other data format. .obj is a standard 3D file type; so, it shouldn't be a problem. Alternatively you can also download Blender, a free cross-platform 3D modeling applications that does support exporting to .obj

In .obj files, the first two letters of every line tell us what kind of data that line contains. "v" is for a "vertex coordinates" line, "vt" is for a "texture coordinates" line, and "f" is for the mapping line. With this information, I wrote the following function:

function LoadModel(ModelName, CB) {
    var Ajax = new XMLHttpRequest();
    Ajax.onreadystatechange = function () {
        if (Ajax.readyState == 4 && Ajax.status == 200) {
            //Parse Model Data
            var Script = Ajax.responseText.split("\n");

            var Vertices = [];
            var VerticeMap = [];

            var Triangles = [];

            var Textures = [];
            var TextureMap = [];

            var Normals = [];
            var NormalMap = [];

            var Counter = 0;

This function accepts the name of a model and a callback function. The callback accepts four arrays: the vertex, triangle, texture, and normal arrays. I haven't yet covered normals, so you can just ignore them for now. I will go through them in the follow-up article, when we discuss lighting.

The importer starts by creating an XMLHttpRequest object and defining its onreadystatechange event handler. Inside the handler, we split the file into its lines and define a few variables. .obj files first define all the unique coordinates and then defines their order. That is why there are two variables for the vertices, textures, and normals. The counter variable is used to fill in the triangles array because .obj files define the triangles in order.

Next, we have to go through each line of the file and check what kind of line it is:

            for (var I in Script) {
                var Line = Script[I];
                //If Vertice Line
                if (Line.substring(0, 2) == "v ") {
                    var Row = Line.substring(2).split(" ");
                    Vertices.push({
                        X: parseFloat(Row[0]),
                        Y: parseFloat(Row[1]),
                        Z: parseFloat(Row[2])
                    });
                }
                //Texture Line
                else if (Line.substring(0, 2) == "vt") {
                    var Row = Line.substring(3).split(" ");
                    Textures.push({
                        X: parseFloat(Row[0]),
                        Y: parseFloat(Row[1])
                    });
                }
                //Normals Line
                else if (Line.substring(0, 2) == "vn") {
                    var Row = Line.substring(3).split(" ");
                    Normals.push({
                        X: parseFloat(Row[0]),
                        Y: parseFloat(Row[1]),
                        Z: parseFloat(Row[2])
                    });
                }

The first three line types are fairly simple; they contain a list of unique coordinates for the vertices, textures and normals. All we need to do is push these coordinates into their respective arrays. The last kind of line is a bit more complicated because it can contain multiple things. It could contain just vertices, or vertices and textures, or vertices, textures, and normals. As such, we have to check for each of these three cases. The following code does this:

            //Mapping Line
                else if (Line.substring(0, 2) == "f ") {
                    var Row = Line.substring(2).split(" ");
                    for (var T in Row) {
                        //Remove Blank Entries
                        if (Row[T] != "") {
                            //If this is a multi-value entry
                            if (Row[T].indexOf("/") != -1) {
                                //Split the different values
                                var TC = Row[T].split("/");
                                //Increment The Triangles Array
                                Triangles.push(Counter);
                                Counter++;

                                //Insert the Vertices 
                                var index = parseInt(TC[0]) - 1;
                                VerticeMap.push(Vertices[index].X);
                                VerticeMap.push(Vertices[index].Y);
                                VerticeMap.push(Vertices[index].Z);

                                //Insert the Textures
                                index = parseInt(TC[1]) - 1;
                                TextureMap.push(Textures[index].X);
                                TextureMap.push(Textures[index].Y);

                                //If This Entry Has Normals Data
                                if (TC.length > 2) {
                                    //Insert Normals
                                    index = parseInt(TC[2]) - 1;
                                    NormalMap.push(Normals[index].X);
                                    NormalMap.push(Normals[index].Y);
                                    NormalMap.push(Normals[index].Z);
                                }
                            }
                            //For rows with just vertices
                            else {
                                Triangles.push(Counter); //Increment The Triangles Array
                                Counter++;
                                var index = parseInt(Row[T]) - 1;
                                VerticeMap.push(Vertices[index].X);
                                VerticeMap.push(Vertices[index].Y);
                                VerticeMap.push(Vertices[index].Z);
                            }
                        }
                    }
                }

This code is more long than it is complicated. Although I covered the scenario where the .obj file only contains vertex data, our framework requires vertices and texture coordinates. If a .obj file contains only vertex data, you will have to manually add the texture coordinate data to it.

Let's now pass the arrays to the callback function and finish up the LoadModel function:

            }
            //Return The Arrays
            CB(VerticeMap, Triangles, TextureMap, NormalMap);
        }
    }
    Ajax.open("GET", ModelName + ".obj", true);
    Ajax.send();
}

Something you should watch out for is that our WebGL framework is fairly basic and only draws models that are made out of triangles. You may have to edit your 3D models accordingly. Luckily, most 3D applications have a function or plug-in to triangulate your models for you. I made a simple model of a house with my basic modeling skills, and I will include it in the source files for you to use, if you are so inclined.

Now let's modify the Draw function from the last tutorial to incorporate our new 3D object data type:

this.Draw = function (Model) {
    if (Model.Image.ReadyState == true && Model.Ready == false) {
        this.PrepareModel(Model);
    }
    if (Model.Ready) {
        this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Model.Vertices);
        this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);
        this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Model.TextureMap);
        this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);

        this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles);

        //Generate The Perspective Matrix
        var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 1000.0);

        var TransformMatrix = Model.GetTransforms();
        //Set slot 0 as the active Texture
        this.GL.activeTexture(this.GL.TEXTURE0);

        //Load in the Texture To Memory
        this.GL.bindTexture(this.GL.TEXTURE_2D, Model.Image);

        //Update The Texture Sampler in the fragment shader to use slot 0
        this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, "uSampler"), 0);

        //Set The Perspective and Transformation Matrices
        var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, "PerspectiveMatrix");
        this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix));

        var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, "TransformationMatrix");
        this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix));

        //Draw The Triangles
        this.GL.drawElements(this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0);
    }
};

The new draw function first checks if the model has been prepared for WebGL. If the texture has loaded, it will prepare the model for drawing. We will get to the PrepareModel function in a minute. If the model is ready, it will connect its buffers to the shaders and load the perspective and transformation matrices like it did before. The only real difference is that it now takes all the data from the model object.

The PrepareModel function just converts the texture and data arrays into WebGL compatible variables. Here is the function; add it right before the draw function:

this.PrepareModel = function (Model) {
    Model.Image = this.LoadTexture(Model.Image);

    //Convert Arrays to buffers
    var Buffer = this.GL.createBuffer();

    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Buffer);
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Model.Vertices), this.GL.STATIC_DRAW);
    Model.Vertices = Buffer;

    Buffer = this.GL.createBuffer();

    this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, Buffer);
    this.GL.bufferData(this.GL.ELEMENT_ARRAY_BUFFER, new Uint16Array(Model.Triangles), this.GL.STATIC_DRAW);
    Model.Triangles = Buffer;

    Buffer = this.GL.createBuffer();

    this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Buffer);
    this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Model.TextureMap), this.GL.STATIC_DRAW);
    Model.TextureMap = Buffer;

    Model.Ready = true;
};

Now our framework is ready and we can move on to the HTML page.


The HTML Page

You can erase everything that is inside the script tags as we can now write the code more concisely thanks to our new GLObject data type.

This is the complete JavaScript:

var GL;
var Building;

function Ready() {
    GL = new WebGL("GLCanvas", "FragmentShader", "VertexShader");
    LoadModel("House", function (VerticeMap, Triangles, TextureMap) {
        Building = new GLObject(VerticeMap, Triangles, TextureMap, "House.png");

        Building.Pos.Z = 650;

        //My Model Was a bit too big
        Building.Scale.X = 0.5;
        Building.Scale.Y = 0.5;
        Building.Scale.Z = 0.5;

        //And Backwards
        Building.Rotation.Y = 180;

        setInterval(Update, 33);
    });
}

function Update() {
    Building.Rotation.Y += 0.2
    GL.Draw(Building);
}

We load a model and tell the page to update it at around thirty times per second. The Update function rotates the model on the Y axis, which is accomplished by updating the object's Y Rotation property. My model was a bit too big for the WebGL scene and it was backwards, so I needed to perform some adjustments in code.

Unless you are making some kind of cinematic WebGL presentation, you are probably going to want to add some controls. Let's look at how we can add some keyboard controls to our application.


Keyboard Controls

This is not really a WebGL technique as much as a native JavaScript feature, but it is handy for controlling and positioning your 3D models. All you have to do is add an event listener to the keyboard's keydown or keyup events and check which key was pressed. Each key has a special code, and a good way to find out which code corresponds to the key is to log the key codes to the console when the event fires. So go to the area where I loaded the model, and add the following code right after the setInterval line:

document.onkeydown = handleKeyDown;

This will set the function handleKeyDown to handle the keydown event. Here is the code for the handleKeyDown function:

function handleKeyDown(event) {
    //You can uncomment the next line to find out each key's code
    //alert(event.keyCode);

    if (event.keyCode == 37) {
        //Left Arrow Key
        Building.Pos.X -= 4;
    } else if (event.keyCode == 38) {
        //Up Arrow Key
        Building.Pos.Y += 4;
    } else if (event.keyCode == 39) {
        //Right Arrow Key
        Building.Pos.X += 4;
    } else if (event.keyCode == 40) {
        //Down Arrow Key
        Building.Pos.Y -= 4;
    }
}

All this function does is update the object's properties; the WebGL framework takes care of the rest.


Conclusion

We're not done! In the third and final part of this mini-series, we will review different kinds of lighting, and how to tie it all up with some 2D stuff!

Thank you for reading, and, as always, if you have any questions, feel free to leave a comment below!

Series Navigation«WebGL Essentials: Part IWebGL Essentials: Part III»

Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://54grad.de Karsten

    It would be nice to see a simple demo page with the output!

    I think it is easier to understand and rebuild, if you have a working reference.
    Ok, I could download the demo code – but you know: tl’dr -> show me somehow if it is worth to download the source code and learn the lesson ;-)

  • http://www.facebook.com/hr.afshari hamid

    thank you for download link

  • ian

    Hi Gabriel,

    I haven’t gotten into the tutorial yet but Windows apparently has an issue with the .obj extension. I can see it in a regular explorer window if I open the folder but my browsers give a 404 not found error in firebug and sublimetext doesn’t see the file either. Just to test I renamed it to House.txt and then changed the line
    Ajax.open(“GET”, ModelName + “.txt”, true);

    and it works.

    Is there something like a system driver or dll or something that would make browsers on Windows see the obj file?

    Thanks

    • Gabriel
      Author

      I got my hands on a Windows computer and tested it out, and it seems to read the .obj file just fine. I’m using windows 7 with chrome as the browser. If you are using the same, then the only thing I can think of is if you have file extensions in windows set to hidden and the file is really “model.obj.obj” or you added the .obj in the html file when you were passing in the models name so it doubled it once it got to the loading script. Let me know if this helps.

  • Robert Wildling

    …just in case somebody didn’t insert the “flip positive Y” call in the LoadTexture function, because in the first lesson it was said to be optional … well: here it is a must, otherwise the model gets totally distorted…

    Thanks again for that tutorial! Looking forward to the third part!

    • Gabriel
      Author

      Ya I forgot to mention that, it’s not really mandatory but the program I used to make the texture has reversed Ys compared to WebGL.

      • Robert Wildling

        Any chance you could comment on my question from part 1?

        What does this strange clear(16384 | 256) mean? Esp the ” | 256″ part?

        Thanks!

      • Gabriel
        Author

        Ya no problem I answered it there

  • http://antongerdelan.net anton

    nice tutorials. noticed that you are showing row-major homogeneous matrices in the diagrams, but your vertex shader is using column-major matrix multiplication:
    “gl_Position = PerspectiveMatrix * TransformationMatrix * vec4(VertexPosition, 1.0);”
    should be easy enough to explain…

    • Gabriel
      Author

      your right, it’s because when I learnt this stuff it was for directX, so this is how I picture it in my head.

  • Aim

    Hey.

    I can’t get this working with my own model. Are there any guidelines I should remember when using my own model?

    I exported a cube in .OBJ from 3dsmax with triangulated faces and a cube unwrap. Couldn’t get it to work using the source code, let alone the code I made from following the tutorial step by step.

    • Aim

      I think it’s definitely something I’m doing wrong with exporting. I loaded your model into my code and it worked. Here is my export dialog options: http://i.imgur.com/P8Zjd.jpg

      • Gabriel
        Author

        I’m not completely sure I would have to see the actual obj file. You can send me a message on twitter: @gabrielmanricks and We can try and troubleshoot it better.