WebGL from Scratch: Textures, part 2

Last time, I showed how to texture a mesh, but I can’t really deny that you might not be convinced that there was really a mesh under there: it just looked like a square with some perspective applied.

In this post, I’m going to demonstrate a simple hack that partially makes up for WebGL’s lack of a functional equivalent to OpenGL’s glPolygonMode by adding barycentric coordinates to each triangle. The basic premise is simple: when the vertex shader sees each vertex, it will have a barycentric coordinate of [1,0,0], [0,1,0], or [0,0,1]. Each triangle will always have all three, although the order doesn’t matter. The fragment shader will see the interpolated values of these coordinates, meaning that points that are too far away from an edge can be identified and discarded.

Unfortunately, this implementation is invasive: each vertex carries an additional three floats defining for for each vertex. In the next post, I’ll show how to do the same thing in a non-invasive way (although admittedly in a way that requires a regular repeating pattern to be effective; more on that later).

As usual, this code relies upon gl-matrix, and also the bricks.png image I used last time. You’ll likely need to deliver the page with a local web server, whether it’s industrial strengh or just python -m SimpleHTTPServer.

The Code

<!doctype html>
<html>
  <head>
    <title>Hacking WebGL</title>
    <script type="text/javascript" src="gl-matrix.js"></script>
    <script id="vertex-shader" type="x-shader/x-vertex">
      precision mediump float;

      uniform mat4 modelMatrix, viewMatrix, projectionMatrix;

      attribute vec3 pos;
      attribute vec2 texCoords;
      attribute vec3 barycentric;

      varying vec2 tc;
      varying vec3 bary;

      void main() {
        tc = texCoords;
        bary = barycentric;
        gl_Position = 
          projectionMatrix * viewMatrix *
          modelMatrix * vec4(pos, 1.0);
      }      
    </script>
    <script id="fragment-shader" type="x-shader/x-fragment">
      precision mediump float;

      uniform bool wireframe;
      uniform sampler2D image;
      uniform float wireframeThickness;

      varying vec2 tc;
      varying vec3 bary;

      void main() {
        if ( wireframe ) {
          if ( bary[0] > wireframeThickness &&
               bary[1] > wireframeThickness &&
               bary[2] > wireframeThickness ) {
            discard;
          }
        } 
        gl_FragColor = texture2D(image, tc.st);
      }
    </script>
    <script type="text/javascript" src="gl-matrix.js"></script>
    <script type="text/javascript">

    function createCanvas() {
      var canvas = document.createElement('canvas');
      document.getElementById('content').appendChild(canvas);
      return canvas;      
    }

    function createProgram(gl, shaderSpecs) {
      var program = gl.createProgram();
      for ( var i = 0 ; i < shaderSpecs.length ; i++ ) {
        var spec = shaderSpecs[i];
        var shader = gl.createShader(spec.type);
        gl.shaderSource(
          shader, document.getElementById(spec.container).text
        );
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
          throw gl.getShaderInfoLog(shader);
        }
        gl.attachShader(program, shader);
        gl.deleteShader(shader);
      }
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw gl.getProgramInfoLog(program);
      }
      return program;
    }

    function render(gl, scene) {
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.useProgram(scene.program);
      gl.uniformMatrix4fv(
        scene.program.modelMatrixUniform, gl.FALSE,
        scene.object.modelMatrix);
      gl.bindBuffer(gl.ARRAY_BUFFER, scene.object.buffer);
      gl.bindTexture(gl.TEXTURE_2D, scene.object.texture);

      gl.drawArrays(
        scene.object.primitiveType, 0,
        scene.object.vertexCount);

      gl.bindTexture(gl.TEXTURE_2D, null);

      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.useProgram(null);
      requestAnimationFrame(function() {
        render(gl, scene);
      });
    }

    function createFlatMesh(gl) {
      var MAX_ROWS=32, MAX_COLS=32;
      var points = [];

      for ( var r = 0 ; r <= MAX_ROWS ; r++ ) {
        for ( var c = 0 ; c <= MAX_COLS ; c++ ) {
          points.push({
            location: [-0.75 + (1.5 / MAX_COLS) * c, 
                        0.75 - (1.5 / MAX_ROWS) * r,
                        0.0],
            texture: [1.0 / MAX_COLS * c,
                      1.0 / MAX_ROWS * r]
          });
        }
      }
      var OFFSET = function(R,C) {
        return ((R) * ((MAX_COLS)+1) + (C));
      };
      var
        vertices = [],
        rotations = [-1,-1,-1,0,1,1,1,0,-1,-1,-1,0,1,1,1,0];
      for ( var r = 1 ; r <= MAX_ROWS ; r += 2 ) {
        for ( var c = 1 ; c <= MAX_COLS ; c += 2 ) {
          for ( var i = 0 ; i < 8 ; i++ ) {
            var off1 = OFFSET(r, c);
            var off2 = OFFSET(r + rotations[i],   c + rotations[i+6]);
            var off3 = OFFSET(r + rotations[i+1], c + rotations[i+7]);
            Array.prototype.push.apply(
              vertices, points[off1].location);
            Array.prototype.push.apply(
              vertices, points[off1].texture);
            vertices.push(1,0,0);
            Array.prototype.push.apply(
              vertices, points[off2].location);
            Array.prototype.push.apply(
              vertices, points[off2].texture);
            vertices.push(0,1,0);
            Array.prototype.push.apply(
              vertices, points[off3].location);
            Array.prototype.push.apply(
              vertices, points[off3].texture);
            vertices.push(0,0,1);
          }
        }
      }

      var buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(
        gl.ARRAY_BUFFER, new Float32Array(vertices),
        gl.STATIC_DRAW);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);

      return {
        buffer: buffer,
        primitiveType: gl.TRIANGLES,
        vertexCount: vertices.length / 8
      }
    }

    function loadTexture(name, gl, mesh, andThenFn) {
      var texture = gl.createTexture();
      var image = new Image();
      image.onload = function() {
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
        gl.texImage2D(
          gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.texParameteri(
          gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(
          gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(
          gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(
          gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);
        mesh.texture = texture;
        andThenFn();
      }
      image.src = name;
    }

    function init() {
      var canvas = createCanvas();
      var gl = canvas.getContext('experimental-webgl');
      var resize = function() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        gl.viewport(0,0,canvas.width,canvas.height);
      };
      window.addEventListener('resize', resize);

      resize();

      gl.enable(gl.DEPTH_TEST);
      gl.clearColor(0.0, 0.0, 0.0, 0.0);

      var mesh = createFlatMesh(gl);

      var program = createProgram(
        gl,
        [{container: 'vertex-shader', type: gl.VERTEX_SHADER},
         {container: 'fragment-shader', type: gl.FRAGMENT_SHADER}]);

      canvas.addEventListener('click', function() {
        gl.useProgram(program);
        var existingValue = gl.getUniform(
          program,
          program.wireframeUniform);
        gl.uniform1i(program.wireframeUniform, !existingValue);
        gl.useProgram(null);
      });

      var projectionMatrix = mat4.create();
      mat4.perspective(
        projectionMatrix, 0.75, canvas.width/canvas.height,
        0.1, 100);
      var viewMatrix = mat4.create();
      var modelMatrix = mat4.create();
      mat4.translate(modelMatrix, modelMatrix, [0,0,-2]);
      mat4.rotate(modelMatrix, modelMatrix, -1, [1,0,0]);

      mesh.modelMatrix = modelMatrix;

      gl.useProgram(program);

      program.modelMatrixUniform =
        gl.getUniformLocation(program, 'modelMatrix');
      program.viewMatrixUniform =
        gl.getUniformLocation(program, 'viewMatrix');
      program.projectionMatrixUniform =
        gl.getUniformLocation(program, 'projectionMatrix');
      program.wireframeUniform =
        gl.getUniformLocation(program, 'wireframe');
      program.wireframeThicknessUniform =
        gl.getUniformLocation(program, 'wireframeThickness');

      gl.uniform1i(program.wireframeUniform, 1);
      gl.uniform1f(program.wireframeThicknessUniform, 0.1);
      
      gl.uniformMatrix4fv(
        program.projectionMatrixUniform, gl.FALSE,
        projectionMatrix);
      gl.uniformMatrix4fv(
        program.viewMatrixUniform, gl.FALSE, viewMatrix);

      gl.bindBuffer(gl.ARRAY_BUFFER, mesh.buffer);

      program.positionAttribute =
        gl.getAttribLocation(program, 'pos');
      program.textureCoordsAttribute =
        gl.getAttribLocation(program, 'texCoords');
      program.barycentricAttribute =
        gl.getAttribLocation(program, 'barycentric');
      gl.enableVertexAttribArray(program.positionAttribute);
      gl.enableVertexAttribArray(program.textureCoordsAttribute);
      gl.enableVertexAttribArray(program.barycentricAttribute);
      gl.vertexAttribPointer(
        program.positionAttribute, 3, gl.FLOAT, false,
        8 * Float32Array.BYTES_PER_ELEMENT,
        0);
      gl.vertexAttribPointer(
        program.textureCoordsAttribute, 2, gl.FLOAT, false,
        8 * Float32Array.BYTES_PER_ELEMENT,
        3 * Float32Array.BYTES_PER_ELEMENT);
      gl.vertexAttribPointer(
        program.barycentricAttribute, 3, gl.FLOAT, false,
        8 * Float32Array.BYTES_PER_ELEMENT,
        5 * Float32Array.BYTES_PER_ELEMENT);

      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.useProgram(null);

      loadTexture('bricks.png', gl, mesh,
        function() {
          requestAnimationFrame(function() {
            render(gl, {
              program: program,
              object: mesh
            });
          })
        });
    }
    </script>
  </head>
  <body onLoad="init()">
    <div id="content">
    </div>
  </body>
</html>

What’s New

The vertex shader has a barycentric vec3 attribute, which it assigns to a bary varying for the fragment shader. The fragment shader checks to see if a given fragment is too far from an edge location, potentially discarding it.

The barycentric attribute values are spliced into the vertex data array in createFlatMesh, and mapped with getAttribLocation/enableVertexAttribArray/vertexAttribPointer as usual.

Finally, a click event handler is attached to the <canvas>, which flips the value of the wireframe uniform in the fragment shader: this controls whether to display the whole image or the wireframe. Note two things here: 1) the current value of a uniform is read from GPU memory by calling getUniform; and 2) the program had to be made active to be able to do this.

Things to Try

  • Add a control to adjust the wireframe thickness
  • Draw another mesh at a different angle so that it partially overlays the existing one. Are the holes in the mesh transparent or drawn with the background colour?

What’s Next

Time-based distortion of the mesh—animating a simple ripple effect.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s