WebGL / Tutorials / #2 Square

Covering the Canvas

This is a follow-up to WebGL Tutorial #1 Triangle. We’ll start from what we built in the previous tutorial. This time we’ll draw a square that covers the entire canvas and make some improvements to the code along the way.

View the demo or jump ahead and edit the code.

The next tutorial, #3 Gradient, builds on this one.

Cleanup

First let’s address the repetitiveness of the shader setup code:

  
    const vertexShader = gl.createShader(gl.VERTEX_SHADER);
    gl.shaderSource(vertexShader, vertexCode.trim());
    gl.compileShader(vertexShader);
    if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
      throw gl.getShaderInfoLog(vertexShader);
    }

    const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
    gl.shaderSource(fragmentShader, fragmentCode.trim());
    gl.compileShader(fragmentShader);
    if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
      throw gl.getShaderInfoLog(fragmentShader);
    }

    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    ...
  

We can make a reusable function to do this:

  
    function createShader(shaderType, sourceCode) {
      const shader = gl.createShader(shaderType);
      gl.shaderSource(shader, sourceCode.trim());
      gl.compileShader(shader);
      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        throw gl.getShaderInfoLog(shader);
      }
      return shader;
    }
  

And use it to setup the program:

  
    const program = gl.createProgram();
    gl.attachShader(program, createShader(gl.VERTEX_SHADER, vertexCode));
    gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fragmentCode));
  

Drawing a Square

With that out of the way, let’s draw a square. We can simply add three more vertices to our list of vertices:

  
    const vertices = [
      [-1, -1, 0],
      [1, -1, 0],
      [1, 1, 0],
      [1, 1, 0],
      [-1, 1, 0],
      [-1, -1, 0],
    ];
  

This can be improved though. When we call gl.drawArrays(gl.TRIANGLES, 0, vertices.length);, the gl.TRIANGLES parameter tells WebGL to draw one triangle for every three vertices. There are other options, including gl.TRIANGLE_STRIP.

With triangle strips, every vertex in the list defines a triangle with the following two vertices (as long as there are at least two vertices following it). For example, the list [A,B,C,D,E] defines three triangles: [A,B,C] , [B,C,D], and [C,D,E].

Let’s try it:

  
    const vertices = [
      [-1, -1, 0],
      [1, -1, 0],
      [1, 1, 0],
      [-1, 1, 0],
    ];
    ...
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length);
  

This doesn’t draw a square though. The order of our vertices are wrong:

When we connect vertices 1,2,3 and 2,3,4 into triangles, they overlap instead of covering the canvas. We need to draw the vertices in a “Z” shape to get the intended result:

  
    const vertices = [
      [-1, -1, 0],
      [1, -1, 0],
      [-1, 1, 0],
      [1, 1, 0],
    ];
  

And now we have a square again:

2D Vertices

We can simplify this more. We’re always setting the z coordinate to 0 because we’re drawing 2D. In the vertex shader, z defaults to 0 if we don’t provide it, so we don’t need to provide it:

  
    const vertices = [
      [-1, -1],
      [1, -1],
      [-1, 1],
      [1, 1],
    ];
  

For this to work, we have to tell gl.vertexAttribPointer() that we are giving it 2D vertices by changing the second parameter to 2:

  
    gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);
  

Result

It’s not much to look at, but now we have the entire canvas to play around with:

Here’s the full HTML page with code. Changes to the previous tutorial are highlighted.

  
  <!DOCTYPE html>
  <html>
  <body>
    <canvas id="canvas" width="500" height="500"></canvas>

    <script id="vertex" type="x-shader/x-vertex">
      #version 300 es

      in vec4 vertexPosition;

      void main() {
        gl_Position = vertexPosition;
      }
    </script>

    <script id="fragment" type="x-shader/x-fragment">
      #version 300 es
      precision highp float;

      out vec4 fragColor;

      void main() {
        fragColor = vec4(1, 0, 0, 1);
      }
    </script>

    <script>
      const canvas = document.getElementById("canvas");
      const vertexCode = document.getElementById("vertex").textContent;
      const fragmentCode = document.getElementById("fragment").textContent;

      const gl = canvas.getContext("webgl2");
      if (!gl) throw "WebGL2 not supported";

      function createShader(shaderType, sourceCode) {
        const shader = gl.createShader(shaderType);
        gl.shaderSource(shader, sourceCode.trim());
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          throw gl.getShaderInfoLog(shader);
        }
        return shader;
      }

      const program = gl.createProgram();
      gl.attachShader(program, createShader(gl.VERTEX_SHADER, vertexCode));
      gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, fragmentCode));
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw gl.getProgramInfoLog(program);
      }
      gl.useProgram(program);

      const vertices = [
        [-1, -1],
        [1, -1],
        [-1, 1],
        [1, 1],
      ];
      const vertexData = new Float32Array(vertices.flat());
      gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
      gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);

      const vertexPosition = gl.getAttribLocation(program, "vertexPosition");
      gl.enableVertexAttribArray(vertexPosition);
      gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);

      gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length);
    </script>
  </body>
  </html>
  

View the demo.

Try it on CodePen.

Go to the next tutorial, #3 Gradient.