Use GLSL to write WebGL shaders

Learn to write WebGL shaders to create high performance 2D and 3D graphics that run directly in the GPU.

Shaders and variables

Shaders are written using GLSL, a C-like code that runs in the GPU and manipulates the vertex and texture data you create in JavaScript. There are two kinds of shaders:

  • vertex shaders, which operate on the coordinate data contained in vertex buffers
  • fragment shaders, which determine the color of each pixel

Shader source code is defined either in the HTML file using <script> tags or defined as strings in your JavaScript code.

Shaders are the programmable part of the rendering pipeline, and can be as simple or complex as needed to get the effect you want.

Unlike in JavaScript, GLSL variables are typed. When creating a variable you don't use var, but a type as you would in C++ or C#. There are several types such as int, float, vec, or mat. There are also qualifiers such as uniform, attribute, and varying, which set how a variable is used. When assigning numbers to typed variables, you must use the correct notation. For example, 6 and 6.0 are essentially the same in JavaScript. In GLSL, their types are int and float respectively. You can say int x = 6, or float x = 6.0, but not float x = 6 .

Data coming in from your JavaScript code is passed by two types of qualifier variables: uniforms and attributes.

  • Uniform variables are global variables. They can be used by either the vertex or fragment shader, and define values that stay the same during the run of the program. An example is a value to use to scale vertex points.
  • Attributes are variables that relate to a specific vertex point. Attributes can only be used in the vertex shader, and could be used to set a specific color for each vertex point.

There's a third qualifier variable, the varying variable, that's declared only in the GLSL shader code. A varying variable is set in the vertex shader and consumed in the fragment shader.

The output of each shader is passed with special variables. For the vertex shader, the position of the current vertex is passed to the fragment shader using gl_Position. The gl_Position variable is a four dimension (vec4) variable that contains the x, y, z, and w values for a vertex.

The output of the fragment shader is gl_FragColor, a four dimensional variable, or vec4. gl_FragColor represents an R,G,B,A value for the pixel being rendered after it's processed by the shader code.

Building a the vertex and fragment shader

In the example, there are two shader programs created; one with the texture (or photo), and another with the red fragment shader. Depending on whether the Show triangle mesh or Show rendered photo radio buttons are checked in the UI, one or the other is active.

// Load the GLSL source written in the HTML file.
// Create a program with the two shaders
this.lineprogram = loadProgram(gl, getShader(gl, "2d-vertex-shader"), getShader(gl, "red"));

  // Tell webGL to use this program
gl.useProgram(this.lineprogram);

This section of code calls functions to create the vertex and fragment shaders, and to create the shader program. On return, the shader program that was just created is made active by useProgram.

The vertex and fragment shaders are created using the getShader function in the Warp example.

// Loads a shader from a script tag
// Parameters:
//   WebGL context
//   id of script element containing the shader to load
function getShader(gl, id) {
  var shaderScript = document.getElementById(id);

  // error - element with supplied id couldn't be retrieved
  if (!shaderScript) {
    return null;
  }

  // If successful, build a string representing the shader source
  var str = "";
  var k = shaderScript.firstChild;
  while (k) {
    if (k.nodeType == 3) {
      str += k.textContent;
    }
    k = k.nextSibling;
  }

  var shader;

  // Create shaders based on the type we set
  //   note: these types are commonly used, but not required
  if (shaderScript.type == "x-shader/x-fragment") {
    shader = gl.createShader(gl.FRAGMENT_SHADER);
  } else if (shaderScript.type == "x-shader/x-vertex") {
    shader = gl.createShader(gl.VERTEX_SHADER);
  } else {
    return null;
  }

  gl.shaderSource(shader, str);
  gl.compileShader(shader);

  // Check the compile status, return an error if failed
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.log(gl.getShaderInfoLog(shader));
    return null;
  }

  return shader;
}

The getShader function is passed the WebGLRenderingContext object and the id of the shaders script element. The getShader follows these steps and returns either a fragment or vertex shader:

  1. Gets the shader source code from the script tags in HTML using getElementById and the id attribute.
  2. GetShader builds a string (str) from the shader code a text node at a time. The function continues until the text runs out.
  3. The type of shader is identified by using the type attribute on the <script> element. The type attributes of the script tag (x-shader/x-fragment, x-shader/x-vertex) aren't part of any standard, and are ignored by HTML. However, you can still read them using the type attribute in JavaScript.
  4. Depending on the type, an empty fragment or vertex shader object is created using createShader.
  5. The source code is added using shaderSource and the shader source code string created earlier.
  6. The shader object is compiled using compileShader.
  7. The compile status is checked with getShaderParameter and the COMPILE_STATUS flag.
  8. If the COMPILE_STATUS flag is false, the shader failed to compile, and getShader returns with null.
  9. If the COMPILE_STATUS flag is true, then getShader returns with the shader object.

To ensure the shaders that are returned attach to the program object successfully, the getProgramParameter method is used with the gl.LINK_STATUS constant to get the status. This method returns true if the shaders are linked to the program object correctly, or a false otherwise. If the shader isn't linked correctly, the last error is retrieved using getProgramInfoLog and shown in the console. The program is deleted by WebGL if there's an error. If there's no error, the program object s returned.

Creating a shader program

The Warp example creates a shader program object using the loadprogram function, and attaches the vertex and fragment shaders that are created with getShader function.

The shader program links the vertex and fragment shader and runs on the GPU. It contains the variables needed to pass data between JavaScript on the CPU and the GPU. Like many processes in WebGL, creating and using a shader program requires several steps. The following is the loadProgram function in the Warp example. The loadProgram function creates the shader program, and attaches and links the vertex and fragment shaders.

function loadProgram(gl, vertexShader, fragmentShader)
{
  // create a progam object
  var program = gl.createProgram();

  // attach the two shaders 
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);

  // link everything 
  gl.linkProgram(program);

  // Check the link status
  var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (!linked) {

    // An error occurred while linking
    var lastError = gl.getProgramInfoLog(program);
    console.warn("Error in program linking:" + lastError);

    gl.deleteProgram(program);
    return null;
  }

  // if all is well, return the program object
  return program;
};

The loadProgram follows these steps when running:

  1. Create a program object with createProgram.
  2. Attach the vertex shader and fragment shader that were passed to loadProgram using attachShader.
  3. Link the shaders to the shader program object using linkProgram.
  4. Check the link status with getProgramParameter, passing the shader program and the LINK_STATUS flag.
  5. If the link status is false, get the last error using getProgramInfoLog and print to the console. Delete the program using deleteProgram, and return null.
  6. If the link status is true, return the shader program.

Get data from JavaScript into a shader

While the shader code has been created, we still need to feed it with data to create our graphics. Vector data gets passed from JavaScript to the shader through an attribute variable. Shaders can take attributes of a number of types, such as vec2, vec3, vec4, bool, int, and so on. The attribute is declared in the shader, and accessed from JavaScript by binding a buffer in the shader program. To access a shader variable, you need its location. The location of the attribute variable a_texCoord, declared in the shader, is returned using getAttribLocation.

  // Look up where the vertex data needs to go.
this.texCoordLocation2 = gl.getAttribLocation(this.lineprogram, "a_texCoord");

  // Provide texture coordinates for the rectangle.
this.texCoordBuffer2 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer2);
      
// Create a buffer and set it use the array set up above.
// Set it to be modified once, use many.
// createRedGrid sets up the vector array itself.        
gl.bufferData(gl.ARRAY_BUFFER, createRedGrid(), gl.STATIC_DRAW); // Fill buffer data

// Turns on the vertex attributes in the GPU program. 
gl.enableVertexAttribArray(this.texCoordLocation2);

// Set up the data format for the vertex array - set to points (x/y). 
// Use floats.
gl.vertexAttribPointer(this.texCoordLocation2, 2, gl.FLOAT, false, 0, 0);
}

This section of code follows these steps:

  1. Using createBuffer, a buffer is created to store the vector array.
  2. The vector array is bound to the shader program using bindBuffer. Binding a buffer makes the buffer active, and available to subsequent calls to bufferData.
  3. The buffer is then filled with our vector array using bufferData. The gl.STATIC_DRAW flag tells WebGL that this buffer data will be written to one time, but used many times.
  4. The vertex attribute is enabled using enableVertexAttribArray. This tells the shader program to use this as the active buffer.
  5. Lastly, WebGL needs to know what the format of the array is using vertexAttribPointer. This method passes the location of the array, texCoordLocation2, the number of dimensions for each element in the array (2), and the type of data (gl.FLOAT). The rest of the parameters are defaults with normalized set to false, and the stride and offset both set to 0.

If these steps are successful, the shader program now has a buffer for the vector points of the shape we're building. The next section goes into some more detail on shader code itself.

Shader code

The shader programs work on one vertex point at a time through an array of vertices to make changes to the mesh. The incoming vertex points are passed into the vertex shader with the attribute a_texCoord, and passed to the fragment shader from the vertex shader as a varying v_texCoord variable. In the example here both variables are two dimensional vectors, but they can be any type.

The output of the calculated x,y coordinates in the vertex shader is passed to fragment shader through gl_Position. gl_Position is a vec4 variable, and in the Warp demo, represents the x and y coordinate. The z coordinate is set to 0 for the center of the coordinate system, and the last, or w parameter is set to 1. The w parameter is used with a projection matrix. The x, y, and z coordinates are multiplied by the projection matrix to project or place the point into 3D space. When set to 1, the x, y, and z coordinates are not projected.

Uniform variables are changed for the start and end of a drag for movements.

  <script id="2d-vertex-shader" type="x-shader/x-vertex">

     // outgoing coordinate
    varying vec2 v_texCoord;

    // incoming coordinate (point)
    attribute vec2 a_texCoord;  
    
    // maximum number of changes to grid
    #define MAXPOINTS 10 
    
    uniform vec2 p1[MAXPOINTS];    // Where the drag started
    uniform vec2 p2[MAXPOINTS];    // Where the drag ended

    void main() { 
      
      v_texCoord = a_texCoord;  
      // Set up position variable with current coordinate normalized from 0 - 1 to -1 to 1 range 
      vec2 position = a_texCoord * 2.0 - 1.0; 

      for (int i = 0; i < MAXPOINTS; i++) // loop through 
      {
        float dragdistance = distance(p1[i], p2[i]); // Calculate the distance between two start and end of mouse drag for each of the drags
        float mydistance = distance(p1[i], position);  // Calculate the distance between the start of the mouse drag and the last position  
        if (mydistance < dragdistance) 
        {
          vec2 maxdistort = (p2[i] - p1[i]) / 4.0;    // only affect vertices within 4 x the drag distance ( 
          float normalizeddistance = mydistance / dragdistance;                
          float normalizedimpact = (cos(normalizeddistance*3.14159265359)+1.0)/2.0;
          position += (maxdistort * normalizedimpact);  
        }
      }
    // gl_Position always specifies where to render this vector 
      gl_Position = vec4(position, 0.0, 1.0);     // x,y,z,
    }
  </script>

When you drag a mouse cursor on the photo or grid, the mouse coordinates are returned by the mouse events. The coordinates are converted from pixel based coordinates to values between -1 and 1 and loaded into an array (p1, p2). The mesh vertex points are recalculated based on the difference between the coordinates of the click and the dragging (p1, p2). These values are passed by the uniform variables. In this example, the area that gets distorted is limited to a circular area of nearby points that's calculated in the shader code. The size of the circle affected depends on the length of the drag. When the mesh is distorted, the photo or texture will have its pixels updated to give us the warp effect.

There are two fragment shaders; one uses the photo, and the other provides the red color for the grid lines. The active fragment shader depends on the mode (grid or image radio buttons) you set.

<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

  // uniform to use for texture 
  uniform sampler2D u_image;

  // Output of the vertex shader 
  varying vec2 v_texCoord;

  void main() {
    // gl_FragColor always specifies the color to make the current pixel 
    gl_FragColor = texture2D(u_image, v_texCoord);                                                   
  }
</script>

The fragment shader is a little simpler than the vertex shader. The purpose of the fragment shader is to determine the color of a pixel. That can be through the varying variable it receives from the vertex shader, or a solid color. In the fragment shader for the grid, the incoming varying variable is declared but ignored. The shader only sets the output color to a fixed value, and never uses the variable.

For the fragment shader that loads an image, a uniform sampler2D variable called u_image is defined to accept the incoming pixels. We see the u_image variable in the JavaScript initialization code, where the image is loaded. The data type for u_image is sampler2D which specifies that the data is a 2D texture.

The vertex shader passes a reference to the texture to the fragment shader using a varying variable called v_texCoord. The v_texCoord variable contains a mapping into the image of where to get the color from the texture for the current coordinate being processed. In the red color shader, the v_texCoord variable is declared, but never used, and the outgoing value for the pixel is just set to red. All the lines are colored the same without extrapolating colors or blending.

The output color from the fragment shader, whether from a texture or a single color, is assigned to the gl_FragColor variable. The gl_FragColor variable is a standard output parameter which tells the renderbuffer the color to make the current pixel.

The image fragment shader uses the GLSL function texture2D for output. The command gl_FragColor = texture2D(u_image, v_texCoord) uses the texture2D function to do a lookup into the texture (u_image) using the coordinates specified by v_texCoord and passes it to gl_FragColor. The GPU then processes and displays that pixel.

Using a shader program

The loadProgram function just described takes us back to the init function in our example where the variables lineprogram and pictureprogram are bound to the context object.

In the render function, the shader programs are bound with buffers and uniform data, and rendered. This following example shows how the programs, data, and attributes are all connected together and rendered.

//  Clear color buffer and set it to light gray
    gl.clearColor(1.0, 1.0, 1.0, 0.5);
    gl.clear(this.gl.COLOR_BUFFER_BIT);
            
// This draws either the grid or the photo for stretching
    if (document.getElementById("renderLines").checked)
    {
      gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer2);

      gl.useProgram(this.lineprogram);

      gl.uniform2fv(gl.getUniformLocation(this.lineprogram, "p1"), p1);
      gl.uniform2fv(gl.getUniformLocation(this.lineprogram, "p2"), p2);

      gl.vertexAttribPointer(this.texCoordLocation2, 2, gl.FLOAT, false, 0, 0);

      gl.enableVertexAttribArray(this.texCoordLocation2);

      gl.drawArrays(gl.LINES, 0, resolution * resolution * 10);
    }
    else
    { 
      gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
      
      gl.useProgram(this.pictureprogram);


      gl.uniform2fv(gl.getUniformLocation(this.pictureprogram, "p1"), p1);
      gl.uniform2fv(gl.getUniformLocation(this.pictureprogram, "p2"), p2);

      gl.vertexAttribPointer(this.texCoordLocation, 2, gl.FLOAT, false, 0, 0);

      gl.enableVertexAttribArray(this.texCoordLocation);


      gl.drawArrays(gl.TRIANGLES, 0, resolution * resolution * 2 * 3);
    }

An if/then conditional statement checks whether the user wants to show the photo or the grid, and loads the appropriate shader program. The code for the line and photo rendering is the same with the exception of the shader program that loads, and the description of what to draw (triangles or lines) that is given to the drawArrays method. Here's how it works:

  • The vertex buffer is bound for use with the program using bindBuffer.
  • The lineprogram or pictureprogram are set to be used for the current rendering state using useProgram.
  • The starting and ending point array variables are assigned to the program using uniform2fv.
  • The format and location of the vertex buffer is set with vertexAttribPointer.
  • The vertex buffer array is enabled using enableVertexAttribArray.
  • Lastly the program is drawn using the vertex array and textures using drawArrays as either lines or triangles.

That sums up the shader code in the example. This example and topic have simplified the process of creating a shader, just touching on the basics. For more in-depth information on shaders, see WebGL demos, references, and resources. Loading the photo is covered in Create a WebGL texture from a photo.

To help learn more about creating shader programs, take a look at the Shader Creation Tool on the Babylon.js website. It offers a number of templates and meshes to experiment with in a vertex and fragment shader.

Create a WebGL texture from a photo

UI support

WebGL demos, references, and resources