WebGL Game Development Tutorials

Loading WebGL Shaders - WebGL Shaders Tutorial

Before we go into the source code examples, I wanted to show you what the heck we're even doing here. Here is a live WebGL demo of a pretty basic WebGL shader in action.

WebGL Shader Demo

This demo below here has the model of a simple car that I created in Blender and saved in PLY format (by the way loading Blender's PLY format is explained here) spinning around the Y axis. It is using a simple vertex and fragment shaders, examples of which will be shown in this tutorial.

You can see some of the car vertices are colored in blotchy yellow color, that was applied only to demonstrate color interpolation across object's vertices.

I am not a professional modeler (to say the least) and I am sure you can find far better models than this. But the shader code does get the job done! To see the source code for this WebGL shader demo, just crack open the HTML source for this page in your browser or continue reading this WebGL tutorial.

Loading shaders is instrumental for rendering anything interesting. But it might be a bit tricky to set up. Especially if you want to do it the right way. Hard-coding shaders is tedious. Instead, loading shader from file (in other words, loading a shader externally) should be implemented.

In this tutorial I will provide a generic WebGL shader pair (vertex and fragment) loader code that you can use when making your own games or similar 3D applications. But we'll also take things one step further and write a function to automate multiple shader loading process.

Vertex + Fragment = Shader Program

All WebGL shaders consist of a vertex and fragment shader pair. Together they form what's known as a shader program. In your frame render loop, you will switch these shader programs on and code that displays your 3D model will then use the currently selected shader program to render its polygons.

In this shader tutorial, let's walk through the complete source code for loading shaders in WebGL. This tutorial is tailored for loading multiple shaders, so that in the future you can easily add your own.

Shader Program Manager Class

Our ShaderProgramManager JavaScript class will contain a listing of all the shaders in our program that we may need to use in our game loop at any time:

// Set to true once all shaders finished loading asynchronously
window.ShadersFinishedLoading = false;

class ShaderProgramManager { // Globally available shader programs
    constructor() {
        this.standardProgram = null;      // Draw static point in the middle
        this.globalDrawingProgram = null; // Draw static point globally 
        this.vertexColorProgram = null;   // Draw object with interpolated vertex colors
        this.textureProgram = null;       // Draw a textured (uv-mapped) object
        this.lightProgram = null;
        this.moveProgram = null;
    }
}

Here I added a few un-initialized shader objects and set them to null to get started.

Whenever you want to add a new shader, simply add another property name to the "this" object and name it whatever you want (that is, a name that closely resembles what the shader actually does.)

Let's instantiate our ShaderProgramManager class and store it in the object Shader. (I like to keep my object names as simple as possible.)

    var Shader = new ShaderProgramManager();        // Create shader program manager

The function CreateShaderProgram, that we are about take a look at, takes one argument gl which is the WebGL rendering context. Remember, we get this object from the step where we initialized WebGL in a previous tutorial.

    gl = GetWebGLContext(canvas);

For the complete listing of this function, refer to the previous WebGL initialization tutorial.

Before I show you how to load a list of shaders from a folder (or a URL location,) let me introduce you to the basic idea behind initializing shaders by providing two examples of very basic WebGL shaders.

WebGL Shader Example

Below I m listing two WebGL shader examples. This function is provided here for reference only (these shader examples will not be used in the rest of this tutorial). It contains a pair of generic shaders created from a simple string of text. As we move forward in this tutorial I will show you how to load multiple WebGL shaders efficiently from a URL address. (From a link that is.)

 function CreateShaderProgram(gl) {

     // Draw point at x = 0, y = 0, z = 0
     var v = 'void main() { gl_Position = vec4(0.0, 0.0, 0.0, 1); gl_PointSize = 10.0; }';
     var f = 'void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }'; // Red
     Shader.standardProgram = InitializeShader(gl, v, f);

     // Draw a point at an arbitrary location, determined globally by the JavaScript application
     v = 'attribute vec4 a_Position; void main() { gl_Position = a_Position; gl_PointSize = 10.0; }';
     f = 'void main() { gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); }'; // Green
     Shader.globalDrawingProgram = InitializeShader(gl, v, f);
 }

You will notice that the shader itself is written in GLSL language. So you see the declaration of main function which is its entry point (one for each of the vertex and fragment shaders.) The content of the shader is unimportant here. I just wanted to demonstrate how to load multiple shaders from a string of text. And in this function two shaders are loaded one after another. You can repeat this process for as many shaders as you want.

The vertex shader's output value is gl_Position, which refers to the position of the currently processing vertex of your 3D model. This value is automatically passed to the fragment shader.

In the fragment shader component, you will see gl_FragColor equal some color. (Colors in GLSL are represented by a 3D vector (3 XYZ values,) used as a RGB triplet, such as vec3(1.0f, 1.0f, 1.0f) or vec4(1.0f, 1.0f, 1.0f, 1.0f) where you have RGB and extra A (for color alpha.) This will be the color of the currently processing pixel on your texture (also known as texel, or fragment.) If you set it to a flat color, your entire model will be rendered in that color without any light or shading effect.

Note again, that CreateShaderProgram takes the WebGL context object via the gl argument that was previously initialized.

This is also our first encounter with the function InitializeShader, but we haven't looked at it yet. It will be explained as we move forward. For now just know that we will need to pass it WebGL context and the two strings of text for each shader component per shader.

Automating Shader Loading Code

To automate the shader loading process we will create two lists. The first list will refer to shader filename pairs (without the .vs and .frag extension, to avoid repetitive code, as these extensions will be assumed.) The second list will refer to the shader program name in our Shader (shader manager) object. This way we can refer to the loaded pair as a unique shader name when the time comes to enable it in our program.

At one point, you will get really tired of writing your shaders in a string of text. You will want to store them in separate files. You may have even downloaded someone else's WebGL shaders and now you want to plug them into your own program. In this section I will show you how to automate the shader loading process. All you have to do after this is simply create your shaders and store them in a folder reserved specifically for shader files and the JavaScript code below will take care of the rest.

Once all shaders are fully loaded (which I will show how to check for in just a moment) they will automatically become available for use in your program. But before we move on, let's enumerate our shader pairs. We could have loaded their names from our shader folder and used their filenames as the shader names themselves, but for the sake of clarity in this example, the names will be simply listed in this array. (How you wish to store them in your own WebGL program is entirely up to you.)

var shaders = [ // Enumerate shader filenames
    "standard", // this assumes "standard.vs" & "standard.frag" are available in "shaders" directory
    "global",   // this assumes "global.vs" & "global.frag" are available in "shaders" directory
    "vertex",
    "texture",
    "light",
    "directional",
    "point",
    "sprite",
    "spritesheet"
];

An idea: You can potentially generate this list by reading filenames from your shader folder and removing .vs and .frag extensions. For now let's hard-code the list of shader pair names by putting them into an array of strings called shaders.

For example, the list above assumes that both files <standard.vs> and <standard.frag> actually exist in your shader folder, because it has an entry called <standard>. The same goes for global, vertex, texture, etc.

Now let's create another list, this time for storing shader program names called shader_name.

The names on this list will refer to the Shader Manager Object called Shader we created earlier in a previous step.

var shader_name = [ // Enumerate shader program names
    "standardProgram",
    "globalDrawingProgram",
    "vertexColorProgram",
    "textureMapProgram",
    "lightProgram",
    "directionalProgram", // directional light
    "pointProgram",       // point light
    "spriteProgram",
    "spritesheetProgram"
];

These are just WebGL shader examples already implemented elsewhere in other tutorials on this site.

Now all we have to do is walk through each name in shaders array, and call LoadShader. The function CreateShadersFromFile will accomplish that for us by using JavaScript's for-in loop.

// Scroll through the list, loading shader pairs
function CreateShadersFromFile( gl ) {
    
    var cache_Bust = new Date().getTime()/1000|0;
    
    for (i in shaders)
        LoadShader(gl, shader_name[i], shaders[i] + ".vs?v=" + cache_Bust, shaders[i] + ".frag?v=" + cache_Bust,
            i // pass in the index of the currently loading shader,
              // this way we can determine when last shader has finished loading
        );
}

Here a JavaScript cache busting trick (cache_Bust) is used so that if you ever update your shader file, the new version will always be loaded (and not the old one from browser's cache) which might result in occasional confusion, if this step is not taken care of.

We've gone through the list of available shaders. Now what? Here is the complete listing of LoadShader function.

function LoadShader(gl, shaderName, filenameVertexShader, filenameFragmentShader, index)
{
    // Folder where your shaders are located
    var ShaderDirectory = "shaders";

    var filename_vs = ShaderDirectory + "/" + filenameVertexShader;
    var filename_fs = ShaderDirectory + "/" + filenameFragmentShader;

    var v = ""; // Placeholders for the shader pair
    var f = "";

    // Now execute two Ajax calls in a row to grab the vertex and
    // fragment shaders from the file location

    var xmlhttp = new XMLHttpRequest();

    // Execute first Ajax call to load the vertex shader
    xmlhttp.onreadystatechange = function() {
        if (xmlhttp.readyState == XMLHttpRequest.DONE) {
            if (xmlhttp.status == 200) {

                v = xmlhttp.responseText;

                // Execute second Ajax call to load the fragment shader
                var xmlhttp2 = new XMLHttpRequest();
                xmlhttp2.onreadystatechange = function () {
                    if (xmlhttp2.readyState == XMLHttpRequest.DONE)
                        if (xmlhttp2.status == 200) {

                            f = xmlhttp2.responseText;

                            Shader[ shaderName ] = InitializeShader(gl, v, f, filenameVertexShader, filenameFragmentShader);
                            // Is this the last shader in the queue?
                            // If so, execute "all shaders loaded" event
                            if (index == shaders.length - 1) {

                                setTimeout(function () {

                                    window.ShadersFinishedLoading = true;

                                }, 500); // .5 sec delay
                            }
                        }
                };
                xmlhttp2.open("GET", filename_fs, true);
                xmlhttp2.send();
            }
        }
    };
    xmlhttp.open("GET", filename_vs, true);
    xmlhttp.send();
}

This function uses a chain of asynchronous requests, until each shader on the list is loaded. Once done, the global variable window.ShadersFinishedLoading will be set to true, making it easy for our program to check whether all shaders have fully finished downloading and initializing.

Speaking of initializing... the function above also does a call to InitializeShader once the content of the files is read.

The function InitializeShader is at the very core of creating and adding shaders to your WebGL program. You may have as well skipped everything prior to this part of the tutorial and simply ran InitializeShader individually, and it would still work. But... your shader loading procedure would not have been automated. This is why I had to cover all those other subjects first.

The arguments of InitializeShader are:

  • gl - An initialized WebGL rendering context
  • source_vs - Vertex Shader source file (GLSL program in text format)
  • source_frag - Fragment Shader source file (GLSL program in text format)
  • fv - Filename of the vertex shader
  • ff - Filename of the fragment shader

And to be honest, the filenames (fv, and ff) are not even needed in this step. They are only used for error reporting. If something goes wrong with loading the shader, at least you know which file is the culprit.

function InitializeShader(gl, source_vs, source_frag, fv, ff)
{
    ErrorMessage = "Initializing Shader Program: <" + fv + ">, <" + ff + ">";

    var shader_vs = gl.createShader(gl.VERTEX_SHADER);
    var shader_frag = gl.createShader(gl.FRAGMENT_SHADER);

    gl.shaderSource(shader_vs, source_vs);
    gl.shaderSource(shader_frag, source_frag);

    gl.compileShader(shader_vs);
    gl.compileShader(shader_frag);

    var error = false;

    // Compile vertex shader
    if (!gl.getShaderParameter(shader_vs, gl.COMPILE_STATUS)) {
        ErrorMessage += gl.getShaderInfoLog(shader_vs);
        error = true;
    }

    // Compile fragment shader
    if (!gl.getShaderParameter(shader_frag, gl.COMPILE_STATUS)) {
        ErrorMessage += gl.getShaderInfoLog(shader_frag);
        error = true;
    }

    // Create shader program consisting of shader pair
    program = gl.createProgram();

    var ret = gl.getProgramInfoLog(program);

    if (ret != "")
        ErrorMessage += ret;

    // Attach shaders to the program; these methods do not have a return value
    gl.attachShader(program, shader_vs);
    gl.attachShader(program, shader_frag);

    // Link the program - returns 0 if an error occurs
    if (gl.linkProgram(program) == 0) {
        ErrorMessage += "\r\ngl.linkProgram(program) failed with error code 0.";
        error = true;
    }
 
    if (error)  {
        console.log(ErrorMessage + ' ...failed to initialize shader.');
        return false;
    } else {
        console.log(ErrorMessage + ' ...shader successfully created.');
        return program; // Return created program
    }
}

And there you have it. You can use InitializeShader separately, by preloading the shader into the Shader manager object and giving it a unique name:

Shader[ shaderName ] = InitializeShader(gl, v, f, filenameVertexShader, filenameFragmentShader);

Or you can use it together with the automatic ajax pre-loader from earlier in this tutorial, which makes absolutely the same call. All of the steps and WebGL functions are clearly explained in comments in the source code above.

Lastly, once the shader is loaded, you can enable it from your game loop by using gl.useProgram command:

    gl.useProgram( Shader.shaderName );

Or alternatively (exactly the same thing as far as JavaScript goes):

    gl.useProgram( Shader["shaderName"] );

This tutorial was never intended to be perfect! I only hope that I provided enough educational value to get you started with WebGL shaders. You can sculpt the source code provided here to fit your own coding style.

Download WebGL Shader Creation Source Code from My GitHub Repository

You can grab a copy of this (and all other examples from my book and tutorials on this site) from my GitHub repository.

Write the code yourself or download it from my WebGL Tutorial Source Code Repository on GitHub.

Note, there is a reason why it's best to automatically preload your shaders. Remember, WebGL requires loading textures and models with unknown size too. This means, there isn't always a way of knowing at what time all those other assets will complete downloading. When checking for completeness of asset availability, we need to check that all textures, models and shaders are fully loaded, and only then proceed to initialize our WebGL app or game.

WebGL Shaders Video Tutorial

In addition to this WebGL shader tutorial article, I also created this video titled WebGL shaders tutorial (WebGL shaders in 11 minutes) on YouTube – for those who like to learn visually.

I hope you enjoyed this tutorial... See you in another one!

WebGL Book: A WebGL Tutorial Reference Book
WebGL Book - A WebGL Tutorial Reference Book

If tutorials on this site are not enough, or you simply like reading from a physical book or a digital device (Kindle, iPad, tablets, etc.) check out WebGL Book. Written by the author of tutorials on this site.

This book is a WebGL tutorial and a reference that guides the reader through the process of setting up and initializing WebGL, drawing 3D primitives and creating 3D computer games.

Preorder Here
© 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.

Lyrics Haven: a song lyrics website, with clean printable lyrics react js tutorials react js elements react js components vue js tutorials angular js tutorials