Search code examples
canvaswebglperspectivecamera3d-model

In webgl, the perspective cameras are added to the canvas or to the 3D model itself?


I have a canvas with controls like zoom, rotate etc. I want to capture the movement of the model from 4 sides: front, back, left and right.

My question is, can I fix 4 cameras on the canvas which can view and display the changing scene on 3 sides while I manipulate one side (front)?

I have read and re-read blogs and tutorials to understand how this works but unfortunately they haven't helped much!

I do not have any code to show


Solution

  • Neither WebGL nor Canvas have any concept of a camera or models. Cameras and models are concepts implemented by you or some library you're using on top of canvas or WebGL.

    You linked to an article in your comments.

    The camera is just a calculation YOU make. So if you want a different view make a different calculation. there are infinite ways to do that

    The code in that article does this at the bottom

        var cameraMatrix = m4.lookAt(cameraPosition, fPosition, up);
    

    So write some code to change cameraPosition and fPosition like

    switch (viewMode) {
      case 'up':
        cameraMatrix = m4.lookAt(upCameraPosition, upCameraTarget, up);
        break;
      case 'front':
        cameraMatrix = m4.lookAt(frontCameraPosition, frontCameraTarget, up);
        break;
      case 'side':
        cameraMatrix = m4.lookAt(sideCameraPosition, sideCameraTarget, up);
        break;
      ...
    }
    

    as one of the 1000s of ways you could change the code.

    Another way

    const viewModes = {
      up:    { cameraPosition: [123,456,532], target: [12,35,23], up:[0, 1, 0], },
      front: { cameraPosition: [ 23, 56,-32], target: [12,35,23], up:[0, 1, 0], },
      side:  { cameraPosition: [323,156, -2], target: [12,35,23], up:[0, 1, 0], },
    };
    
    const {cameraPosition, target, up} = viewModes[viewMode];
    var cameraMatrix = m4.lookAt(cameraPosition, target, up);
    

    Yet another would be making a scene graph, and using different nodes to generate camera matrices.

    Another way would be making a Camera class. Then make multiple instances of that class and choose one to draw with.

    Yet another would be to combine any of the techniques mentioned above.

    Here's an example of one of those methods based off the last example in the article you linked to

    "use strict";
    
    function main() {
      // Get A WebGL context
      /** @type {HTMLCanvasElement} */
      var canvas = document.getElementById("canvas");
      var gl = canvas.getContext("webgl");
      if (!gl) {
        return;
      }
    
      // setup GLSL program
      var program = webglUtils.createProgramFromScripts(gl, ["3d-vertex-shader", "3d-fragment-shader"]);
    
      // look up where the vertex data needs to go.
      var positionLocation = gl.getAttribLocation(program, "a_position");
      var colorLocation = gl.getAttribLocation(program, "a_color");
    
      // lookup uniforms
      var matrixLocation = gl.getUniformLocation(program, "u_matrix");
    
      // Create a buffer to put positions in
      var positionBuffer = gl.createBuffer();
      // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = positionBuffer)
      gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
      // Put geometry data into buffer
      setGeometry(gl);
    
      // Create a buffer to put colors in
      var colorBuffer = gl.createBuffer();
      // Bind it to ARRAY_BUFFER (think of it as ARRAY_BUFFER = colorBuffer)
      gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
      // Put color data into buffer
      setColors(gl);
    
      function radToDeg(r) {
        return r * 180 / Math.PI;
      }
    
      function degToRad(d) {
        return d * Math.PI / 180;
      }
    
      var cameraAngleRadians = degToRad(0);
      var fieldOfViewRadians = degToRad(60);
    
      // Setup a ui.
      const settings = {
        mode: 'front',
      };
      
      document.querySelectorAll('input').forEach((elem) => {
        elem.addEventListener('change', (e) => {
          settings.mode = e.target.value;
          drawScene();
        });
      });
      
      const viewModes = {
        'front': { position: [   0,   0, 200], target: [0, 0, 0], up: [0, 1, 0], },
        'left':  { position: [-200,   0,   0], target: [0, 0, 0], up: [0, 1, 0], },
        'right': { position: [ 200,   0,   0], target: [0, 0, 0], up: [0, 1, 0], },
        'down':  { position: [   0, 400,   0], target: [0, 0, 0], up: [0, 0, -1], },
      };
      
      drawScene();
    
      
      // Draw the scene.
      function drawScene() {
        webglUtils.resizeCanvasToDisplaySize(gl.canvas);
    
        // Tell WebGL how to convert from clip space to pixels
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
    
        // Clear the canvas AND the depth buffer.
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    
        // Turn on culling. By default backfacing triangles
        // will be culled.
        gl.enable(gl.CULL_FACE);
    
        // Enable the depth buffer
        gl.enable(gl.DEPTH_TEST);
    
        // Tell it to use our program (pair of shaders)
        gl.useProgram(program);
    
        // Turn on the position attribute
        gl.enableVertexAttribArray(positionLocation);
    
        // Bind the position buffer.
        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    
        // Tell the position attribute how to get data out of positionBuffer (ARRAY_BUFFER)
        var size = 3;          // 3 components per iteration
        var type = gl.FLOAT;   // the data is 32bit floats
        var normalize = false; // don't normalize the data
        var stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
        var offset = 0;        // start at the beginning of the buffer
        gl.vertexAttribPointer(
            positionLocation, size, type, normalize, stride, offset);
    
        // Turn on the color attribute
        gl.enableVertexAttribArray(colorLocation);
    
        // Bind the color buffer.
        gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
    
        // Tell the attribute how to get data out of colorBuffer (ARRAY_BUFFER)
        var size = 3;                 // 3 components per iteration
        var type = gl.UNSIGNED_BYTE;  // the data is 8bit unsigned values
        var normalize = true;         // normalize the data (convert from 0-255 to 0-1)
        var stride = 0;               // 0 = move forward size * sizeof(type) each iteration to get the next position
        var offset = 0;               // start at the beginning of the buffer
        gl.vertexAttribPointer(
            colorLocation, size, type, normalize, stride, offset);
    
    
        var numFs = 5;
        var radius = 200;
    
        // Compute the projection matrix
        var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
        var zNear = 1;
        var zFar = 2000;
        var projectionMatrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
    
        // Compute the camera's matrix using look at.
        const viewMode = viewModes[settings.mode];
        var cameraMatrix = m4.lookAt(
            viewMode.position, 
            viewMode.target, 
            viewMode.up);
    
        // Make a view matrix from the camera matrix
        var viewMatrix = m4.inverse(cameraMatrix);
    
        // Compute a view projection matrix
        var viewProjectionMatrix = m4.multiply(projectionMatrix, viewMatrix);
    
        for (var ii = 0; ii < numFs; ++ii) {
          var angle = ii * Math.PI * 2 / numFs;
          var x = Math.cos(angle) * radius;
          var y = Math.sin(angle) * radius;
    
          // starting with the view projection matrix
          // compute a matrix for the F
          var matrix = m4.translate(viewProjectionMatrix, x, 0, y);
    
          // Set the matrix.
          gl.uniformMatrix4fv(matrixLocation, false, matrix);
    
          // Draw the geometry.
          var primitiveType = gl.TRIANGLES;
          var offset = 0;
          var count = 16 * 6;
          gl.drawArrays(primitiveType, offset, count);
        }
      }
    }
    
    function subtractVectors(a, b) {
      return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
    }
    
    function normalize(v) {
      var length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
      // make sure we don't divide by 0.
      if (length > 0.00001) {
        return [v[0] / length, v[1] / length, v[2] / length];
      } else {
        return [0, 0, 0];
      }
    }
    
    function cross(a, b) {
      return [a[1] * b[2] - a[2] * b[1],
              a[2] * b[0] - a[0] * b[2],
              a[0] * b[1] - a[1] * b[0]];
    }
    
    
    
    var m4 = {
    
      lookAt: function(cameraPosition, target, up) {
        var zAxis = normalize(
            subtractVectors(cameraPosition, target));
        var xAxis = normalize(cross(up, zAxis));
        var yAxis = normalize(cross(zAxis, xAxis));
    
        return [
           xAxis[0], xAxis[1], xAxis[2], 0,
           yAxis[0], yAxis[1], yAxis[2], 0,
           zAxis[0], zAxis[1], zAxis[2], 0,
           cameraPosition[0],
           cameraPosition[1],
           cameraPosition[2],
           1,
        ];
      },
    
      perspective: function(fieldOfViewInRadians, aspect, near, far) {
        var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians);
        var rangeInv = 1.0 / (near - far);
    
        return [
          f / aspect, 0, 0, 0,
          0, f, 0, 0,
          0, 0, (near + far) * rangeInv, -1,
          0, 0, near * far * rangeInv * 2, 0
        ];
      },
    
      projection: function(width, height, depth) {
        // Note: This matrix flips the Y axis so 0 is at the top.
        return [
           2 / width, 0, 0, 0,
           0, -2 / height, 0, 0,
           0, 0, 2 / depth, 0,
          -1, 1, 0, 1,
        ];
      },
    
      multiply: function(a, b) {
        var a00 = a[0 * 4 + 0];
        var a01 = a[0 * 4 + 1];
        var a02 = a[0 * 4 + 2];
        var a03 = a[0 * 4 + 3];
        var a10 = a[1 * 4 + 0];
        var a11 = a[1 * 4 + 1];
        var a12 = a[1 * 4 + 2];
        var a13 = a[1 * 4 + 3];
        var a20 = a[2 * 4 + 0];
        var a21 = a[2 * 4 + 1];
        var a22 = a[2 * 4 + 2];
        var a23 = a[2 * 4 + 3];
        var a30 = a[3 * 4 + 0];
        var a31 = a[3 * 4 + 1];
        var a32 = a[3 * 4 + 2];
        var a33 = a[3 * 4 + 3];
        var b00 = b[0 * 4 + 0];
        var b01 = b[0 * 4 + 1];
        var b02 = b[0 * 4 + 2];
        var b03 = b[0 * 4 + 3];
        var b10 = b[1 * 4 + 0];
        var b11 = b[1 * 4 + 1];
        var b12 = b[1 * 4 + 2];
        var b13 = b[1 * 4 + 3];
        var b20 = b[2 * 4 + 0];
        var b21 = b[2 * 4 + 1];
        var b22 = b[2 * 4 + 2];
        var b23 = b[2 * 4 + 3];
        var b30 = b[3 * 4 + 0];
        var b31 = b[3 * 4 + 1];
        var b32 = b[3 * 4 + 2];
        var b33 = b[3 * 4 + 3];
        return [
          b00 * a00 + b01 * a10 + b02 * a20 + b03 * a30,
          b00 * a01 + b01 * a11 + b02 * a21 + b03 * a31,
          b00 * a02 + b01 * a12 + b02 * a22 + b03 * a32,
          b00 * a03 + b01 * a13 + b02 * a23 + b03 * a33,
          b10 * a00 + b11 * a10 + b12 * a20 + b13 * a30,
          b10 * a01 + b11 * a11 + b12 * a21 + b13 * a31,
          b10 * a02 + b11 * a12 + b12 * a22 + b13 * a32,
          b10 * a03 + b11 * a13 + b12 * a23 + b13 * a33,
          b20 * a00 + b21 * a10 + b22 * a20 + b23 * a30,
          b20 * a01 + b21 * a11 + b22 * a21 + b23 * a31,
          b20 * a02 + b21 * a12 + b22 * a22 + b23 * a32,
          b20 * a03 + b21 * a13 + b22 * a23 + b23 * a33,
          b30 * a00 + b31 * a10 + b32 * a20 + b33 * a30,
          b30 * a01 + b31 * a11 + b32 * a21 + b33 * a31,
          b30 * a02 + b31 * a12 + b32 * a22 + b33 * a32,
          b30 * a03 + b31 * a13 + b32 * a23 + b33 * a33,
        ];
      },
    
      translation: function(tx, ty, tz) {
        return [
           1,  0,  0,  0,
           0,  1,  0,  0,
           0,  0,  1,  0,
           tx, ty, tz, 1,
        ];
      },
    
      xRotation: function(angleInRadians) {
        var c = Math.cos(angleInRadians);
        var s = Math.sin(angleInRadians);
    
        return [
          1, 0, 0, 0,
          0, c, s, 0,
          0, -s, c, 0,
          0, 0, 0, 1,
        ];
      },
    
      yRotation: function(angleInRadians) {
        var c = Math.cos(angleInRadians);
        var s = Math.sin(angleInRadians);
    
        return [
          c, 0, -s, 0,
          0, 1, 0, 0,
          s, 0, c, 0,
          0, 0, 0, 1,
        ];
      },
    
      zRotation: function(angleInRadians) {
        var c = Math.cos(angleInRadians);
        var s = Math.sin(angleInRadians);
    
        return [
           c, s, 0, 0,
          -s, c, 0, 0,
           0, 0, 1, 0,
           0, 0, 0, 1,
        ];
      },
    
      scaling: function(sx, sy, sz) {
        return [
          sx, 0,  0,  0,
          0, sy,  0,  0,
          0,  0, sz,  0,
          0,  0,  0,  1,
        ];
      },
    
      translate: function(m, tx, ty, tz) {
        return m4.multiply(m, m4.translation(tx, ty, tz));
      },
    
      xRotate: function(m, angleInRadians) {
        return m4.multiply(m, m4.xRotation(angleInRadians));
      },
    
      yRotate: function(m, angleInRadians) {
        return m4.multiply(m, m4.yRotation(angleInRadians));
      },
    
      zRotate: function(m, angleInRadians) {
        return m4.multiply(m, m4.zRotation(angleInRadians));
      },
    
      scale: function(m, sx, sy, sz) {
        return m4.multiply(m, m4.scaling(sx, sy, sz));
      },
    
      inverse: function(m) {
        var m00 = m[0 * 4 + 0];
        var m01 = m[0 * 4 + 1];
        var m02 = m[0 * 4 + 2];
        var m03 = m[0 * 4 + 3];
        var m10 = m[1 * 4 + 0];
        var m11 = m[1 * 4 + 1];
        var m12 = m[1 * 4 + 2];
        var m13 = m[1 * 4 + 3];
        var m20 = m[2 * 4 + 0];
        var m21 = m[2 * 4 + 1];
        var m22 = m[2 * 4 + 2];
        var m23 = m[2 * 4 + 3];
        var m30 = m[3 * 4 + 0];
        var m31 = m[3 * 4 + 1];
        var m32 = m[3 * 4 + 2];
        var m33 = m[3 * 4 + 3];
        var tmp_0  = m22 * m33;
        var tmp_1  = m32 * m23;
        var tmp_2  = m12 * m33;
        var tmp_3  = m32 * m13;
        var tmp_4  = m12 * m23;
        var tmp_5  = m22 * m13;
        var tmp_6  = m02 * m33;
        var tmp_7  = m32 * m03;
        var tmp_8  = m02 * m23;
        var tmp_9  = m22 * m03;
        var tmp_10 = m02 * m13;
        var tmp_11 = m12 * m03;
        var tmp_12 = m20 * m31;
        var tmp_13 = m30 * m21;
        var tmp_14 = m10 * m31;
        var tmp_15 = m30 * m11;
        var tmp_16 = m10 * m21;
        var tmp_17 = m20 * m11;
        var tmp_18 = m00 * m31;
        var tmp_19 = m30 * m01;
        var tmp_20 = m00 * m21;
        var tmp_21 = m20 * m01;
        var tmp_22 = m00 * m11;
        var tmp_23 = m10 * m01;
    
        var t0 = (tmp_0 * m11 + tmp_3 * m21 + tmp_4 * m31) -
            (tmp_1 * m11 + tmp_2 * m21 + tmp_5 * m31);
        var t1 = (tmp_1 * m01 + tmp_6 * m21 + tmp_9 * m31) -
            (tmp_0 * m01 + tmp_7 * m21 + tmp_8 * m31);
        var t2 = (tmp_2 * m01 + tmp_7 * m11 + tmp_10 * m31) -
            (tmp_3 * m01 + tmp_6 * m11 + tmp_11 * m31);
        var t3 = (tmp_5 * m01 + tmp_8 * m11 + tmp_11 * m21) -
            (tmp_4 * m01 + tmp_9 * m11 + tmp_10 * m21);
    
        var d = 1.0 / (m00 * t0 + m10 * t1 + m20 * t2 + m30 * t3);
    
        return [
          d * t0,
          d * t1,
          d * t2,
          d * t3,
          d * ((tmp_1 * m10 + tmp_2 * m20 + tmp_5 * m30) -
                (tmp_0 * m10 + tmp_3 * m20 + tmp_4 * m30)),
          d * ((tmp_0 * m00 + tmp_7 * m20 + tmp_8 * m30) -
                (tmp_1 * m00 + tmp_6 * m20 + tmp_9 * m30)),
          d * ((tmp_3 * m00 + tmp_6 * m10 + tmp_11 * m30) -
                (tmp_2 * m00 + tmp_7 * m10 + tmp_10 * m30)),
          d * ((tmp_4 * m00 + tmp_9 * m10 + tmp_10 * m20) -
                (tmp_5 * m00 + tmp_8 * m10 + tmp_11 * m20)),
          d * ((tmp_12 * m13 + tmp_15 * m23 + tmp_16 * m33) -
                (tmp_13 * m13 + tmp_14 * m23 + tmp_17 * m33)),
          d * ((tmp_13 * m03 + tmp_18 * m23 + tmp_21 * m33) -
                (tmp_12 * m03 + tmp_19 * m23 + tmp_20 * m33)),
          d * ((tmp_14 * m03 + tmp_19 * m13 + tmp_22 * m33) -
                (tmp_15 * m03 + tmp_18 * m13 + tmp_23 * m33)),
          d * ((tmp_17 * m03 + tmp_20 * m13 + tmp_23 * m23) -
                (tmp_16 * m03 + tmp_21 * m13 + tmp_22 * m23)),
          d * ((tmp_14 * m22 + tmp_17 * m32 + tmp_13 * m12) -
                (tmp_16 * m32 + tmp_12 * m12 + tmp_15 * m22)),
          d * ((tmp_20 * m32 + tmp_12 * m02 + tmp_19 * m22) -
                (tmp_18 * m22 + tmp_21 * m32 + tmp_13 * m02)),
          d * ((tmp_18 * m12 + tmp_23 * m32 + tmp_15 * m02) -
                (tmp_22 * m32 + tmp_14 * m02 + tmp_19 * m12)),
          d * ((tmp_22 * m22 + tmp_16 * m02 + tmp_21 * m12) -
                (tmp_20 * m12 + tmp_23 * m22 + tmp_17 * m02))
        ];
      },
    
      vectorMultiply: function(v, m) {
        var dst = [];
        for (var i = 0; i < 4; ++i) {
          dst[i] = 0.0;
          for (var j = 0; j < 4; ++j) {
            dst[i] += v[j] * m[j * 4 + i];
          }
        }
        return dst;
      },
    
    };
    
    // Fill the buffer with the values that define a letter 'F'.
    function setGeometry(gl) {
      var positions = new Float32Array([
              // left column front
              0,   0,  0,
              0, 150,  0,
              30,   0,  0,
              0, 150,  0,
              30, 150,  0,
              30,   0,  0,
    
              // top rung front
              30,   0,  0,
              30,  30,  0,
              100,   0,  0,
              30,  30,  0,
              100,  30,  0,
              100,   0,  0,
    
              // middle rung front
              30,  60,  0,
              30,  90,  0,
              67,  60,  0,
              30,  90,  0,
              67,  90,  0,
              67,  60,  0,
    
              // left column back
                0,   0,  30,
               30,   0,  30,
                0, 150,  30,
                0, 150,  30,
               30,   0,  30,
               30, 150,  30,
    
              // top rung back
               30,   0,  30,
              100,   0,  30,
               30,  30,  30,
               30,  30,  30,
              100,   0,  30,
              100,  30,  30,
    
              // middle rung back
               30,  60,  30,
               67,  60,  30,
               30,  90,  30,
               30,  90,  30,
               67,  60,  30,
               67,  90,  30,
    
              // top
                0,   0,   0,
              100,   0,   0,
              100,   0,  30,
                0,   0,   0,
              100,   0,  30,
                0,   0,  30,
    
              // top rung right
              100,   0,   0,
              100,  30,   0,
              100,  30,  30,
              100,   0,   0,
              100,  30,  30,
              100,   0,  30,
    
              // under top rung
              30,   30,   0,
              30,   30,  30,
              100,  30,  30,
              30,   30,   0,
              100,  30,  30,
              100,  30,   0,
    
              // between top rung and middle
              30,   30,   0,
              30,   60,  30,
              30,   30,  30,
              30,   30,   0,
              30,   60,   0,
              30,   60,  30,
    
              // top of middle rung
              30,   60,   0,
              67,   60,  30,
              30,   60,  30,
              30,   60,   0,
              67,   60,   0,
              67,   60,  30,
    
              // right of middle rung
              67,   60,   0,
              67,   90,  30,
              67,   60,  30,
              67,   60,   0,
              67,   90,   0,
              67,   90,  30,
    
              // bottom of middle rung.
              30,   90,   0,
              30,   90,  30,
              67,   90,  30,
              30,   90,   0,
              67,   90,  30,
              67,   90,   0,
    
              // right of bottom
              30,   90,   0,
              30,  150,  30,
              30,   90,  30,
              30,   90,   0,
              30,  150,   0,
              30,  150,  30,
    
              // bottom
              0,   150,   0,
              0,   150,  30,
              30,  150,  30,
              0,   150,   0,
              30,  150,  30,
              30,  150,   0,
    
              // left side
              0,   0,   0,
              0,   0,  30,
              0, 150,  30,
              0,   0,   0,
              0, 150,  30,
              0, 150,   0]);
    
      // Center the F around the origin and Flip it around. We do this because
      // we're in 3D now with and +Y is up where as before when we started with 2D
      // we had +Y as down.
    
      // We could do by changing all the values above but I'm lazy.
      // We could also do it with a matrix at draw time but you should
      // never do stuff at draw time if you can do it at init time.
      var matrix = m4.xRotation(Math.PI);
      matrix = m4.translate(matrix, -50, -75, -15);
    
      for (var ii = 0; ii < positions.length; ii += 3) {
        var vector = m4.vectorMultiply([positions[ii + 0], positions[ii + 1], positions[ii + 2], 1], matrix);
        positions[ii + 0] = vector[0];
        positions[ii + 1] = vector[1];
        positions[ii + 2] = vector[2];
      }
    
      gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
    }
    
    // Fill the buffer with colors for the 'F'.
    function setColors(gl) {
      gl.bufferData(
          gl.ARRAY_BUFFER,
          new Uint8Array([
              // left column front
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
    
              // top rung front
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
    
              // middle rung front
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
            200,  70, 120,
    
              // left column back
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
    
              // top rung back
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
    
              // middle rung back
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
            80, 70, 200,
    
              // top
            70, 200, 210,
            70, 200, 210,
            70, 200, 210,
            70, 200, 210,
            70, 200, 210,
            70, 200, 210,
    
              // top rung right
            200, 200, 70,
            200, 200, 70,
            200, 200, 70,
            200, 200, 70,
            200, 200, 70,
            200, 200, 70,
    
              // under top rung
            210, 100, 70,
            210, 100, 70,
            210, 100, 70,
            210, 100, 70,
            210, 100, 70,
            210, 100, 70,
    
              // between top rung and middle
            210, 160, 70,
            210, 160, 70,
            210, 160, 70,
            210, 160, 70,
            210, 160, 70,
            210, 160, 70,
    
              // top of middle rung
            70, 180, 210,
            70, 180, 210,
            70, 180, 210,
            70, 180, 210,
            70, 180, 210,
            70, 180, 210,
    
              // right of middle rung
            100, 70, 210,
            100, 70, 210,
            100, 70, 210,
            100, 70, 210,
            100, 70, 210,
            100, 70, 210,
    
              // bottom of middle rung.
            76, 210, 100,
            76, 210, 100,
            76, 210, 100,
            76, 210, 100,
            76, 210, 100,
            76, 210, 100,
    
              // right of bottom
            140, 210, 80,
            140, 210, 80,
            140, 210, 80,
            140, 210, 80,
            140, 210, 80,
            140, 210, 80,
    
              // bottom
            90, 130, 110,
            90, 130, 110,
            90, 130, 110,
            90, 130, 110,
            90, 130, 110,
            90, 130, 110,
    
              // left side
            160, 160, 220,
            160, 160, 220,
            160, 160, 220,
            160, 160, 220,
            160, 160, 220,
            160, 160, 220]),
          gl.STATIC_DRAW);
    }
    
    main();
    #uiContainer {
      position: absolute;
      right: 1em;
      top: 1em;
    }
    body {
      margin: 0;
    }
    canvas {
      width: 100vw;
      height: 100vh;
      display: block;
    }
    <canvas id="canvas"></canvas>
    <div id="uiContainer">
      <div id="ui">
        <input type="radio" name="mode" id="front" value="front" checked><label for="front">front</label>
        <input type="radio" name="mode" id="left" value="left"><label for="left">left</label>
        <input type="radio" name="mode" id="right" value="right"><label for="right">right</label>
        <input type="radio" name="mode" id="down" value="down"><label for="down">down</label>
      </div>
    </div>
    <!-- vertex shader -->
    <script id="3d-vertex-shader" type="x-shader/x-vertex">
    attribute vec4 a_position;
    attribute vec4 a_color;
    
    uniform mat4 u_matrix;
    
    varying vec4 v_color;
    
    void main() {
      // Multiply the position by the matrix.
      gl_Position = u_matrix * a_position;
    
      // Pass the color to the fragment shader.
      v_color = a_color;
    }
    </script>
    <!-- fragment shader -->
    <script id="3d-fragment-shader" type="x-shader/x-fragment">
    precision mediump float;
    
    // Passed in from the vertex shader.
    varying vec4 v_color;
    
    void main() {
       gl_FragColor = v_color;
    }
    </script><!--
    for most samples webgl-utils only provides shader compiling/linking and
    canvas resizing because why clutter the examples with code that's the same in every sample.
    See http://webglfundamentals.org/webgl/lessons/webgl-boilerplate.html
    and http://webglfundamentals.org/webgl/lessons/webgl-resizing-the-canvas.html
    for webgl-utils, m3, m4, and webgl-lessons-ui.
    -->
    <script src="https://webglfundamentals.org/webgl/resources/webgl-utils.js"></script>