WebGL Quick Start

WebGL Book: A WebGL Tutorial Reference Book
Easily understand WebGL and learn how to create 3D worlds and games from scratch.

WebGL Gems is the authorative guide on WebGL programming published by Learning Curve - education for software developers. This book in JavaScript programming series clearly, and in easy to follow language, explains fundamental principles behind programming 3D games using the WebGL framework for JavaScript.

Preorder Now in PDF format for Kindle and get a discount.
WebGL Gems is coming out on April 15th, 2017.
The paperback version will be available via this Amazon page, please bookmark it!

Drawing a Vertex-Colored Triangle

In previous example we created a white triangle and rendered it on the screen. Traditionally to create colored or "smooth-shaded" surfaces (which are incredibly useful for imitating how light scatters across a surface) GPUs also enable us to work with color by creating new array bindings for data containing per-vertex color in RGBA format. The A stands for alpha channel.

The previous triangle only bound vertex coordinates and passed it to the GPU. Let's take that code and upgrade it so we can now draw colored triangles. This psychedelic-looking triangle will not accomplish much other than demonstrate how we can pass multiple array buffers representing various data about our geometry.

The purpose of doing this will become much more clear as we gradually move toward constructing entire 3D worlds that have more detail and where shading realism will be drastically increased.

Binding To a Secondary Buffer

You can bind as many buffers as you want representing vertex position (x,y,z), vertex colors (r,g,b), texture and normal coordinates (u, v, and s, t) and tangent space normals (on this a little bit later).

But the point is that WebGL allows us to bind multiple arrays representing various data about our primitive. In addition to vertex coordinates, to add vertex color data for each vertex we have to bind an additional data array to the memory buffer and pass it to the GPU.

The process is exactly the same as in previous WebGL tutorial, with just a few slight modifications. WebGL as well as OpenGL retain "default" or "current" buffer while binding operations take place. This means that we can bind to one buffer at a time. So we will juggle around the code and end up with the source code listing shown below.

While reading other technical books I noticed that I would often get confused when source code was skipped and "..." was used instead to make sure that no unnecessary page space was used to repeat what was already said. I can definitely understand authors of those books. However, that has the drawback of page to page browsing that ultimately slows down the learning process. So I am going to copy the previous source code in its entirety here. However, I will highlight only what was changed so it's easier to process.

If you are viewing this on a non-color Kindle device or a printed book, the highlighted areas are the ones that appear darker than the rest and their comments start with the word "// New". Everything else is exactly the same.

Please note that I am doing something else to the vertex data now. I am initializing vertex and the new color array using native WebGL object Float32Array. These are just like regular JavaScript arrays. The convenience is that we can grab the byte size of each value via Float32Array.BYTES_PER_ELEMENT property, wheres prior to this we used gl.FLOAT.


    var BYTESIZE = vertices.BYTES_PER_ELEMENT;

Here vertices is our array filled with vertex data.

This simply gives us the convenience to refer to the data size when specifying vertex attribute pointers using vertexAttribPointer function. The last two parameters of which are size of the data per vertex and stride.

In our new example size is BYTESIZE * 3 in each case because vertex coordinates are in XYZ format, and colors are in RGB format, each containing 3 unique variables.

vAnd now we are going to demonstrate this with an actual example. After that we will modify our shader code so that we can pass newly bound color data via standard.vs and standard.frag to the GPU. And only then we can render a colored triangle.

Let's first revisit the webGLResourcesLoaded function:


    // An event that fires when all shader resources
    // finish loading in CreateShadersFromFile
    window.webGLResourcesLoaded = function()
    {
        console.log("webGLResourcesLoaded(): " + "All webGL shaders have finished loading!");

        // Specify triangle vertex data:
        var vertices = var Float32Array([
            -0.0,  0.5, 0.0, // Vertex A (x,y,z)
            -0.5, -0.5, 0.0, // Vertex B (x,y,z)
             0.5, -0.5, 0.0  // Vertex C (x,y,z)
        ]);

        // New: Specify colors for each vertex as well
        var colors = new Float32Array([
            1.0, 0.0, 0.0, // Vertex A (r,g,b)
            0.0, 1.0, 0.0, // Vertex B (r,g,b)
            0.0, 0.0, 1.0  // Vertex C (r,g,b)
        ]);

        var indices = [0, 1, 2]; // One index per vertex coordinate

        // Create buffer objects for storing triangle vertex and index data
        var vertexbuffer = gl.createBuffer();
        var colorbuffer = gl.createBuffer(); var// New: also create color buffer
        var indexbuffer = gl.createBuffer();

        var BYTESIZE = vertices.BYTES_PER_ELEMENT;

        // Bind and create enough room for our data on respective buffers

        // Bind vertexbuffer to ARRAY_BUFFER
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexbuffer);
        // Send our vertex data to the buffer using floating point array
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices),
        gl.STATIC_DRAW);
        // Get attribute location
        var coords = gl.getAttribLocation(Shader.vertexColorProgram, "a_Position");
        // Pointer to the currently bound VBO (vertex buffer object)
        gl.vertexAttribPointer(coords, 3, gl.FLOAT, false, BYTESIZE*3, 0);
        gl.enableVertexAttribArray(coords); // Enable it
        // We're done; now we have to unbind the buffer
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        // Bind colorbuffer to ARRAY_BUFFER
        gl.bindBuffer(gl.ARRAY_BUFFER, colorbuffer);
        // Send our color data to the buffer using floating point array
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors),
        gl.STATIC_DRAW);
        // Get attribute location
        var colors = gl.getAttribLocation(Shader.vertexColorProgram, "a_Color");
        // Pointer to the currently bound VBO (vertex buffer object)
        gl.vertexAttribPointer(colors, 3, gl.FLOAT, false, BYTESIZE*3, 0);
        gl.enableVertexAttribArray(colors); // Enable it
        // We're done; now we have to unbind the buffer
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        // Bind it to ELEMENT_ARRAY_BUFFER
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexbuffer);
        // Send index (indices) data to this buffer
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
        // We're done; unbind, we no longer need the buffer object
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

        // Use our standard shader program for rendering this triangle
        gl.useProgram( Shader.vertexColorProgram );

        // Start main drawing loop
        var T = setInterval(function() {

            if (!gl)
                return;

            // Create WebGL canvas
            gl.clearColor(0.0, 0.0, 0.0, 1.0);

            gl.clear(gl.COLOR_BUFFER_BIT);

            // Draw triangle
            gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT,0);

        });
    });

Most notably we're using a new shader here. We can no longer use Shader.standardShader from previous example. It has been replaced with Shader.vertexColorProgram which will be discussed in a bit.

When we bind an array to ARRAY_BUFFER it becomes current. And we can use only one at a time. All functions after this operation will be performed on that buffer. For example, actually sending the data to the buffer using bufferData function. This is why we have to bind vertexbuffer first, attach our vertices array to it, and tie it to a_Position attribute that will be available from within the vertex shader.

Likewise, we will bind colorbuffer to ARRAY_BUFFER once again, and then tie it via a_Color attribute so we can access that data from within the new shader itself.

Once a buffer is bound and connected to our JavaScript array containing either vertices or color data, we now must enable it with gl.enableVertexAttribArray function. This process is repeated for both vertexbuffer and colorbuffer.

Vertex Color Shader

We will now modify our shaders. Note, that we need an entirely new shader here. Not our old standard.vs and standard.frag pair. We're building on them, but if we modify these shaders with new code to support vertex color, our previous demos will cease working. The code will break because they don't pass a_Color attribute and these shaders expect one.

So I created a new pair: vertex.vs and vertex.frag.

vertex.vs

    attribute vec4 a_Position;
    attribute vec4 a_Color;      // New: added vec4 attribute
    varying vec4 color;          // New: this will be passed to fragment shader

    void main() {
        gl_Position = a_Position;
        color = a_Color;         // New: pass it as varying color, not a_Color.
    }

    

Just three new lines were added. a_Color is now passed together with a_Position. I also created varying vec4 color. It is also specified in the fragment shader:

vertex.frag

    precision lowp float;       // New: specify floating-point precision
    varying vec4 color;         // New: receive it from vertex shader

    void main() {
        gl_FragColor = color;
    }

    

We must specify floating point precision. If it is removed the shader will not compile. The precision directive tells the GPU about which type of floating point precision to use while calculating floating-point based operations.

Use highp for vertex positions, mediump for texture coordinates and lowp for colors.

And finally, putting this all together and running it in the browser will produce the following result:

Triangle vertex-color shader example

Here, each tip of our triangle is nicely interpolated between the colors we specified in our colors array. Switch them to any color you'd like and you will see a smooth transition between them. This is done on the GPU by our fragment shader.

Binding One Buffer At A Time

Remember that our fragment shader works with one fragment at a time. However, the vertex coordinates we submitted with vertices array are pseudo-automatically interpolated by the GPU. If we lived in the 90's, you'd have to write procedures that computed these colors manually.

But graphics programming has grown up since then. Many common sub-operations like these are now done on the GPU, without taking away control of the important parts (setting the color to each vertex.)

Ideally, when we deal with real applications or games, we will not be shading our triangles using these psychedelic colors. Instead, they will be shaded across by colors that naturally appear in nature or illuminated by custom light sources. This example only demonstrates the bare polygon shading principle at work.

Adding a New Shader

We're coming to a point where you're starting to add your own shaders to the project.

When you write your own demos using the base source code from this book, let's make sure we're not skipping another important detail when adding new shaders to our engine to avoid all kinds of JavaScript build errors.

Because we're adding an entirely new shader, let's see which code needs to be updated. Here I added new "vertex" string representing vertex.js and vertex.frag pair to the shaders array. We now have 3 shaders!


    var shaders = [ // Enumerate shader filenames, this assumes
                    // "standard.vs" & "standard.frag" are available
                    // in "shaders" directory
        "standard",
        "global",
        "vertex" // New: Added this new filename for the shader pair
    ];

    

Now let's give our new vertex color shader a name we can refer to by in our program.


    var shader_name = [ // Enumerate shader program names
        "standardProgram",
        "globalDrawingProgram",
        "vertexColorProgram"      // New: added new shader name
    ];

I simply named it vertexColorProgram and from now on when we need to use it to draw anything with vertex colors we can issue the following gl command:


    // Use our new vertex color shader program for rendering this triangle
    gl.useProgram( Shader.vertexColorProgram );

And this is pretty much the entire process whenever you want to add a new shader and start using it. Let's quickly review it.

We switched to loading shaders from URLs. Because we are now automatically scanning the "shaders" directory in our project. All shaders found in it will be automatically loaded into our Shader object. We've also given them all a name so they're easy to select and use.

Just make sure that each time you add a new shader you specify its filename in shaders array, write the actual shadername.vs and shadername.frag pair and drop it into "shaders" folder and create a unique name you want to use by storing it in shader_name array. That's it!

Of course this process could be even further automated. And that's the setup for it. For example, you could further scan the "shaders" directory and automatically make the shaders list from scanning filenames without the extensions. I just don't want to complicate the process while teaching the subject.

You could even create your own shader names as the Shader's object properties algorithmically. Because alternatively to object.property_name = 1 assignment in JavaScript, you can assign properties to objects using object["property_name"] notation. This language feature is excellent at automatically loading data from a folder and storing it using variables that partially resemble those filenames. I'll briefly discuss it here and then we'll move on. For example:


    var filename_vs = "texture.vs";
    var propertyName = filename_vs.charAt(0).toUpperCase() + filename;

    object["program" + filename];

And now your shader will be available as:

Shader.programTexture and can be enabled by Shader.use(Shader.programTexture);

And what does this give us? We will never have to bother with creating shader arrays manually by hand just because we wrote a new shader. You'd simply drop your shader pair into the "shader" folder and it would be automatically read from there (you can write a PHP reader that scans the shaders folder) and then the variable names will become auto-magically available from the object itself as in Shader.programShaderfilename. But this is the task I leave to you. If you get this done perhaps you can venture into my GitHub (http://www.github.com/gregsidelnikov) account fork it or submit a pull request from my WebGLTutorials project and then push it back.

A bit later in the book, I will demonstrate how this is achieved, but for another task: loading textures. This is such an important technique that practically solves a lot of frustration. And while at first you might think this is too complex and unnecessary, when you get to feel those practical benefits actually improve your workflow you will fall in love with this method and not want to go back.

You can further optimize your engine because at this point it is still pretty simple and malleable. But even now already it's still a nice way of creating, loading and using new shaders because writing them inside <SCRIPT> tags all the time is a management nightmare.

And this is all we require at this point.

The shaders are automatically loaded from the URL using function CreateShadersFromFile we wrote earlier. Let's recall it from a previous chapter:


    // Scroll through the list, loading shader pairs
    function CreateShadersFromFile( gl ) {
    for (i in shaders)
        LoadShader(gl,
                Shader_name[i],
                shaders[i] + ".vs",
                shaders[i] + ".frag",
                i);
        // Pass in the index "i" of the currently loading shader,
        // this way we can determine when last shader has finished loading
    }
    

As you can see filenames are generated from our shaders array. We never have to specify the pair manually, just its common filename without the extension.

Thus, once our new shader is loaded at the last index [i] in the array, we're ready to continue initialization process and start rendering our object using it.


© 2017 Copyright WebGL Tutorials (webgltutorials.org)

All content and graphics on this website are the property of webgltutorials.org - please provide a back link when referencing on other sites.