My First WebGL Shader

Your browser does not support the canvas tag. This is a static example of what would be seen.

This builds on my existing posts that so far show only how to setup the basic WebGL environment. I won’t cover ground that’s already been covered. Instead I’m going to focus on executing a single draw call, which in turn implies we have a fragment and vertex shader bound, and that we have some vertex data to feed into the front of the shading pipeline.

While we are at it… some of the things we need to do are things pretty much every WebGL script we every write will do, so rather than write the same things over and over we are going to start to break out some of the code into a set of reusable utility functions, and you might notice things are written that way in some cases.

Compiling a shader means taking GLSL source code and turning it into WebGL byte code. WebGL gives us back a handle to the byte code that we can then use to create a shader program object. Code for this looks like this.

  function compileShader(gl, code, type) {
    var shader = gl.createShader(type);
    gl.shaderSource(shader, code);
    gl.compileShader(shader);
    var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
    if (!success) {
      throw "compileShader error:" + gl.getShaderInfoLog(shader);
    }
    return shader;
  }

A shader program object is made up of a vertex and fragment program that are compiled and linked together. Think of it as the output from one being the input to the other. If we assume we have compiled the two parts using the above function, we can link them together using something like the code below, and as our program’s are always going to start with pairs of source code strings rather than shader bytecode objects, we expose another helper function that does all this work for us, taking two strings of GLSL code and returning a shader we can use.

  function createProgram(gl, vs, fs) {
    var program = gl.createProgram();
    gl.attachShader(program, vs);
    gl.attachShader(program, fs);
    gl.linkProgram(program);
    var success = gl.getProgramParameter(program, gl.LINK_STATUS);
    if (!success) {
      throw "createProgram error:" + gl.getProgramInfoLog(program);
    } 
    return program;
  }

  function createShader(gl, vs_code, fs_code) {
    var shader = {};
    var vs = compileShader(gl, vs_code, gl.VERTEX_SHADER);
    var fs = compileShader(gl, fs_code, gl.FRAGMENT_SHADER);
    shader.program = createProgram(gl, vs, fs);
    shader.pos_loc = gl.getAttribLocation(shader.program, "in_pos");
    return shader;
  }

You might notice there that we returned more than just a GL shader program. We looked up an attribute too and cached it’s location alongside the shader. The attribute was called in_pos which we’ll use to feed vertex positions to the shader (more on this later).

Next make a mesh. A simple example of a mesh (that’s also very useful) is a full-screen quad that basically fills the viewport in X and Y. We can make something like that using the code below, and as before we also store some useful data describing the layout of the data buffer that will help when we later bind this object for rendering.

  function createFullscreenQuadMesh(gl) {
    var mesh = {};
    pos_array = new Float32Array([
        -1.0, -1.0, 0.0,
         1.0, -1.0, 0.0,
        -1.0,  1.0, 0.0,
        -1.0,  1.0, 0.0,
         1.0, -1.0, 0.0,
         1.0,  1.0, 0.0
    ]);
    mesh.pos_buffer = gl.createBuffer();
    mesh.pos_type = gl.FLOAT;
    mesh.pos_numComponents = 3;
    mesh.pos_stride = 12;
    gl.bindBuffer(gl.ARRAY_BUFFER, mesh.pos_buffer);
    gl.bufferData(gl.ARRAY_BUFFER, pos_array, gl.STATIC_DRAW);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    return mesh;
  }

Finally we need some shaders. The shaders below are about as simple as it gets. The vertex shader feeds through the position unchanged and the fragment program turns the X and Y values it receives into colours so we can check it actually did something. Javascript allows us to build multi-line strings and for simple shaders we do just that rather than deal with referencing external files.

  var vs_code = 
    "attribute vec3 in_pos;" +
    "varying vec3 v_pos;" +
    "void main(void) {" +
    "  gl_Position = vec4(in_pos, 1.0);" +
    "  v_pos = gl_Position.xyz;" +
    "}";

  var fs_code = 
    "precision mediump float;" +
    "varying vec3 v_pos;" +
    "void main(void) {" +
    "  gl_FragColor = vec4(v_pos.x * 0.5 + 0.5, v_pos.y * 0.5 + 0.5, 1, 1);" +
    "}";

Putting this together… we can now embed our shaders into our WebGL app, then call these two new methods to build some WebGL shaders and buffers.

    shader = createShader(gl, vs_code, fs_code);
    quad_mesh = createFullscreenQuadMesh(gl);

Finally we can execute a draw call by doing the following. Note that some of the data we bound to the objects we made earlier is now proving useful as WebGL needs us to connect the shader input to the vertex data (using pos_loc) and describe the vertex layout so the shader knows how to pull the data in.

    gl.useProgram(shader.program);
    gl.bindBuffer(gl.ARRAY_BUFFER, quad_mesh.pos_buffer);
    gl.vertexAttribPointer(shader.pos_loc,
       quad_mesh.pos_numComponents, 
       quad_mesh.pos_type, 
       false,
       quad_mesh.pos_stride, 
       0);
    gl.enableVertexAttribArray(shader.pos_loc);
    gl.drawArrays(gl.TRIANGLES, 0, 6);

Leave a Reply

Your email address will not be published. Required fields are marked *