Categories

WebGL 2D – Moving Things Around – Part 4: Matrix Magic

This post is a continuation of a series of posts about WebGL. The first started with fundamentals and the previous was about scaling 2D geometry.

In the last 3 posts we went over how to translate geometry, rotate geometry, and scale geometry. Translation, rotation and scale are each considered a type of ‘transformation’. Each of these transformations required changes to the shader and each of the 3 transformations was order dependent. In our previous example we scaled, then rotated, the translated. If we applied those in a different order we’d get a different result.

For example here is a scale of 2, 1, rotation of 30%, and translation of 100, 0.

And here is a translation of 100,0, rotation of 30% and scale of 2, 1

The results are completely different. Even worse, if we needed the second example we’d have to write a different shader that applied the translation, rotation, and scale in our new desired order.

Well, some people way smarter than me, figured out that you can do all the same stuff with matrix math. For 2d we use a 3×3 matrix. A 3×3 matrix is like grid with 9 boxes.

1.0 2.0 3.0
4.0 5.0 6.0
7.0 8.0 9.0

To do the math we multiply the position down the columns of the matrix and add up the results. Our positions only have 2 values, x and y but to do this math we need 3 values so we’ll use 1 for the third value.

in this case our result would be

newX = x * 1.0 + newY = x * 2.0 + extra = x * 3.0 +
y * 4.0 + y * 5.0 + y * 6.0 +
1 * 7.0 1 * 8.0 1 * 9.0

You’re probably looking at that and thinking “WHAT’S THE POINT”. Well, Let’s assume we have a translation. We’ll call the amount we want to translate tx and ty. Let’s make a matrix like this

1.0 0.0 0.0
0.0 1.0 0.0
tx ty 1.0

And now check it out

newX = x * 1.0 + newY = x * 0.0 + extra = x * 0.0 +
y * 0.0 + y * 1.0 + y * 0.0 +
1 * tx 1 * ty 1 * 1.0

If you remember your algebra, we can delete any place that multiplies by zero. Multiplying by 1 effectively does nothing so let’s simplify to see what’s happening

newX = x * 1.0 + newY = x * 0.0 + extra = x * 0.0 +
y * 0.0 + y * 1.0 + y * 0.0 +
1 * tx 1 * ty 1 * 1.0

or more succinctly

newX = x + tx;
newY = y + ty;

And extra we don’t really care about. That looks surprisingly like the translation code from our translation example.

Similarly let’s do rotation. Like we pointed out in the rotation post we just need the sine and cosine of the angle at which we want to rotate so.

s = Math.sin(angleToRotateInRadians);
c = Math.cos(angleToRotateInRadians);

And we build a matrix like this

c -s 0.0
s c 0.0
0.0 0.0 1.0

Applying the matrix we get this

newX = x * c + newY = x * -s + extra = x * 0.0 +
y * s + y * c + y * 0.0 +
1 * 0.0 1 * 0.0 1 * 1.0

Blacking out all multiply by 0s and 1s we get

newX = x * c + newY = x * -s + extra = x * 0.0 +
y * s + y * c + y * 0.0 +
1 * 0.0 1 * 0.0 1 * 1.0

And simplifying we get

newX = x * c + y * s;
newY = x * -s + y * c;

Which is exactly what we had in our rotation sample.

And lastly scale. We’ll call our 2 scale factors sx and sy

And we build a matrix like this

sx 0.0 0.0
0.0 sy 0.0
0.0 0.0 1.0

Applying the matrix we get this

newX = x * sx + newY = x * 0.0 + extra = x * 0.0 +
y * 0.0 + y * sy + y * 0.0 +
1 * 0.0 1 * 0.0 1 * 1.0

which is really

newX = x * sx + newY = x * 0.0 + extra = x * 0.0 +
y * 0.0 + y * sy + y * 0.0 +
1 * 0.0 1 * 0.0 1 * 1.0

which simplified is

newX = x * sx;
newY = y * sy;

Which is the same as our scaling sample.

Now I’m sure you might still be thinking. So what? What’s the point. That seems like a lot of work just to do the same thing we were already doing?

This is where the magic comes in. It turns out we can multiply matrices together and apply all the transformations at once. Let’s assume we have function, matrixMultiply, that takes two matrices, multiplies them and returns the result.

To make things clearer let’s make functions to build matrices for translation, rotation and scale.

function makeTranslation(tx, ty) {
  return [
    1, 0, 0,
    0, 1, 0,
    tx, ty, 1
  ];
}

function makeRotation(angleInRadians) {
  var c = Math.cos(angleInRadians);
  var s = Math.sin(angleInRadians);
  return [
    c,-s, 0,
    s, c, 0,
    0, 0, 1
  ];
}

function makeScale(sx, sy) {
  return [
    sx, 0, 0,
    0, sy, 0,
    0, 0, 1
  ];
}

Now let’s change our shader. The old shader looked like this

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform vec2 u_translation;
uniform vec2 u_rotation;
uniform vec2 u_scale;

void main() {
  // Scale the positon
  vec2 scaledPosition = a_position * u_scale;

  // Rotate the position
  vec2 rotatedPosition = vec2(
     scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
     scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);

  // Add in the translation.
  vec2 position = rotatedPosition + u_translation;
  ...

Our new shader will be much simpler.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;
uniform mat3 u_matrix;

void main() {
  // Multiply the position by the matrix.
  vec2 position = (u_matrix * vec3(a_position, 1)).xy;
  ...

And here’s how we use it

  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Compute the matrices
    var translationMatrix = makeTranslation(translation[0], translation[1]);
    var rotationMatrix = makeRotation(angleInRadians);
    var scaleMatrix = makeScale(scale[0], scale[1]);

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);

    // Set the matrix.
    gl.uniformMatrix3fv(matrixLocation, false, matrix);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 18);
  }

Here’s a sample using our new code. The sliders are the same, translation, rotation and scale. But the way they get used in the shader is much simpler.


click here to open in a separate window

Still, you might be asking, so what? That doesn’t seem like much of a benefit . But, now if we want to change the order we don’t have to write a new shader. We can just change the math.

    ...
    // Multiply the matrices.
    var matrix = matrixMultiply(translationMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, scaleMatrix);
    ...

Here’s that version.


click here to open in a separate window

Being able to apply matrices like this is especially important for hierarchical animation like arms on a body, moons on a planet around a sun, or branches on a tree. For a simple example of hierarchical animation lets draw draw our ‘F’ 5 times but each time lets start with the matrix from the previous ‘F’.

  // Draw the scene.
  function drawScene() {
    // Clear the canvas.
    gl.clear(gl.COLOR_BUFFER_BIT);

    // Compute the matrices
    var translationMatrix = makeTranslation(translation[0], translation[1]);
    var rotationMatrix = makeRotation(angleInRadians);
    var scaleMatrix = makeScale(scale[0], scale[1]);

    // Starting Matrix.
    var matrix = makeIdentity();

    for (var i = 0; i < 5; ++i) {
      // Multiply the matrices.
      matrix = matrixMultiply(matrix, scaleMatrix);
      matrix = matrixMultiply(matrix, rotationMatrix);
      matrix = matrixMultiply(matrix, translationMatrix);

      // Set the matrix.
      gl.uniformMatrix3fv(matrixLocation, false, matrix);

      // Draw the geometry.
      gl.drawArrays(gl.TRIANGLES, 0, 18);
    }
  }

To do this we had introduce the function, makeIdentity, that makes an identity matrix. An identity matrix is a matrix that effectively represents 1.0 so that if you multiply by the identity nothing happens. Just like

X * 1 = X

so too

matrixX * identity = matrixX

Here's the code to make an identity matrix.

function makeIdentity() {
  return [
    1, 0, 0,
    0, 1, 0,
    0, 0, 1
  ];
}

Here's the 5 Fs.


click here to open in a separate window

One more example, In every sample so far our 'F' rotates around its top left corner. This is because the math we are using always rotates around the origin and the top left corner of our 'F' is at the origin, (0, 0)

But now, because we can do matrix math and we can choose the order that transforms are applied we can move the origin before the rest of the transforms are applied.

    // make a matrix that will move the origin of the 'F' to its center.
    var moveOriginMatrix = makeTranslation(-50, -75);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(moveOriginMatrix, scaleMatrix);
    matrix = matrixMultiply(matrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);

Here's that sample. Notice the F rotates and scales around the center.


click here to open in a separate window

Using that technique you can rotate or scale from any point. Now you know how Photoshop or Flash let you move the rotation point.

Let's go even more crazy. If you go back to the first article on WebGL fundamentals you might remember we have code in the shader to convert from pixels to clipspace that looks like this.

  ...
  // convert the rectangle from pixels to 0.0 to 1.0
  vec2 zeroToOne = position / u_resolution;

  // convert from 0->1 to 0->2
  vec2 zeroToTwo = zeroToOne * 2.0;

  // convert from 0->2 to -1->+1 (clipspace)
  vec2 clipSpace = zeroToTwo - 1.0;
  
  gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

If you look at each of those steps in turn, the first step, "convert from pixels to 0.0 to 1.0", is really a scale operation. The second is also a scale operation. The next is a translation and the very last scales Y by -1. We can actually do that all in the matrix we pass into the shader. We could make 2 scale matrices, one to scale by 1.0/resolution, another to scale by 2.0, a 3rd to translate by -1.0,-1.0 and a 4th to scale Y by -1 then multiply them all together but instead, because the math is simple, we'll just make a function that makes a 'projection' matrix for a given resolution directly.

function make2DProjection(width, height) {
  // Note: This matrix flips the Y axis so that 0 is at the top.
  return [
    2 / width, 0, 0,
    0, -2 / height, 0,
    -1, 1, 1
  ];
}

Now we can simplify the shader even more. Here's the entire new vertex shader.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform mat3 u_matrix;

void main() {
  // Multiply the position by the matrix.
  gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
}
</script>

And in JavaScript we need to multiply by the projection matrix

  // Draw the scene.
  function drawScene() {
    ...
    // Compute the matrices
    var projectionMatrix = make2DProjection(canvas.width, canvas.height);
    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);
    matrix = matrixMultiply(matrix, projectionMatrix);
    ...
  }

We also removed the code that set the resolution. With this last step we've gone from a rather complicated shader with 6-7 steps to a very simple shader with only 1 step all do to the magic of matrix math.


click here to open in a separate window

I hope these posts have helped demystified matrix math. I'll move on to 3D next. In 3D matrix math follows the same principles and usage. I started with 2D to hopefully keep it simple to understand.

  • Evan

    Been reading through the whole series. Really well written.

  • Nat

    This is great, thank you.

  • Antonio D’souza

    Your link to the 3D section is broken.

  • http://greggman.com greggman

    thanks. fixed