Retrospective changes to the tutorials: requestAnimationFrame and glMatrix

I’ve just improved the WebGL tutorials:

  • As promised, they now use requestAnimationFrame — or, more precisely, the requestAnimFrame cross-browser compatibility function provided by this Google utility module — to schedule their repaints. This means that they won’t hog CPU/GPU time when the pages are on hidden tabs, which should help avoid getting WebGL an underserved reputation for unnecessary resource-hoggery.
  • I’ve switched from the excellent and general-purpose vector/matrix library Sylvester (plus Vladimir Vukićević’s glUtils.js extensions thereto) to Brandon Jones’s screamingly fast but WebGL-specific glMatrix library.

I’ve got a bunch more changes in the pipeline, and I’m trying to work out which ones would be best done before I spend more time on the next lesson, on picking…

[UPDATE] I’ve just done one other change from the pipeline — removing the animation code from lessons 1 and 2, which don’t use it. It became clear while I did the rewrite for the changes above that having it in those two lessons was unhelpful and confusing, so now I introduce requestAnimationFrame when it’s actually needed, in lesson 3.

[UPDATE] As per a comment by Shy below, pointing out that the normal matrix my lessons use is fundamentally broken, I’ve fixed that too. It’s odd, because I thought I’d made that change after a suggestion by Coolcat long ago — must have dropped the ball there. It’s done now, anyway.

You can leave a response, or trackback from your own site.

28 Responses to “Retrospective changes to the tutorials: requestAnimationFrame and glMatrix”

  1. Shy says:

    I’ve noticed that you’re using the following code to make the normal transformation matrix:

    var normalMatrix = mat4.create();
    mat4.inverse(mvMatrix, normalMatrix);
    mat4.transpose(normalMatrix);

    This is actually wrong. the normal transformation should be the transpose of the inverse of the upper 3×3 of the model view matrix. It should go like this:

    var normalMatrix = mat3.create();
    mat4.toInverseMat3(mvMatrix, normalMatrix);
    normalMatrix = mat3.toMat4(normalMatrix);
    mat4.transpose(normalMatrix);

    (using mat4.transpose becuase glMatrix doesn’t currently have mat3.transpose.
    Your version works because you’re not doing any non-uniform scaling. Also, this version is more efficient.

  2. Bob Hazard says:

    Ooh ooh can’t wait for picking! I want to make a baby starcraft in the browser so I need to click on units and drag rects around them.

    Looking forward to it.

  3. giles says:

    @Shy — great point, and it’s odd — I thought I’d already fixed it after Coolcat pointed out the same error a while back. I’ll fix it again.

    @Bob — that sounds very cool. There was some discussion of picking here a while back, you can see links in the second item in this “around the net” post if you haven’t already.

  4. giles says:

    @Shy — right, that’s done. Lessons updated.

  5. Benoit Jacob says:

    (apparently my comment didn’t come through the first time, retrying)

    Please, everybody, DO NOT use the “inverse transpose” trick to compute the matrix to apply to normal vectors. Instead, just manually zero the top-right 3×1 corners (i.e. the translation part of the matrix).

    The “inverse transpose” trick is very inefficient, can backfire, and hides what one really wants to do.

    First of all, lets recall why the “inverse transpose” trick sort of “works”. The basic reason is that usually the matrix is an isometry so has this form

    r r r t
    r r r t
    r r r t
    0 0 0 1

    Where the top-left 3×3 “r” block is a rotation (or more generally an orthogonal transformation i.e. rotation or anti-rotation), so its inverse is the transose; denoting it “rT”, the whole matrix inverse becomes

    rT rT rT -t
    rT rT rT -t
    rT rT rT -t
    0 0 0 1

    and to the transposed inverse is

    r r r 0
    r r r 0
    r r r 0
    -t -t -t 1

    So, when you apply this to a vector and ignore the 4th coefficient in the result, this has the same effect as applying this matrix,

    r r r 0
    r r r 0
    r r r 0
    0 0 0 1

    Which is what you really wanted.

    Now let’s clarify why the “inverse transpose” trick is junk:

    * it’s very inefficient because computing the inverse is costly, while all what one needed was the much cheaper of the 3 ‘t’ coefficients (top-right 3×1 corner).

    * it can backfire because it relies on the transformation being an isometry. If you eventually add scaling, even uniform scaling, it breaks down.

    * it hides real intentions by replacing the natural action of clearing the transformation part of the matrix by some obscure matrix algebra.

  6. Benoit Jacob says:

    sorry, typo:

    > all what one needed was the much cheaper of the 3 ‘t’ coefficients

    I meant: all what one needed was the much cheaper *clearing* of the 3 ‘t’ coefficients.

  7. Benoit Jacob says:

    What I really recommend to do, to transform normal vectors, is to just use a 0 as 4th coefficient for them, instead of the usual 1, as exemplified here:

    http://people.mozilla.org/~bjacob/iap2011/slides/#slide27

    sample code:

    gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);
    vec4 transformedNormal = uMVMatrix * vec4(aVertexNormal, 0.0);

    see, while we use 1.0 as 4th coeff for the vertex positions, we use 0.0 as 4th coeff for the vertex normals. As a result, vertex positions are affected by the MVMatrix’s translation component while the normal vectors are unaffected.

  8. requestAnimationFrame is cool but not enough, I would love to have a paremeter like the desired frame rate.
    Just because it’s better to have a consistent 30 fps instead of having 60 fps then 30 then 60 ….

    Anyway it’s a good update

  9. Shy says:

    @Benoit Jacob wow this is truly incredible! After years of inverting these matrices… to find out that it only takes a few zeros…

  10. Aleksandar says:

    Thanks for the correction Benoit. I noticed some problems with he normals but I didn’t understand what was wrong. Hopefully, you advise will help me fix it.

  11. Owen Densmore says:

    One controversy is whether or not to have Float32Array’s be the basis for the vectors and matrices, or to use standard javascript Arrays, and then “flatten” and convert via the Float32Array constructor.

    Brandon’s code on my MBAir using Chrome was not always the fastest in the benchmark, CanvasMatrix did better in some tests. I suspect with the V8 google engine, typed arrays are not yet optimized. CanvasMatrix uses javascript Arrays.

    Javascript Arrays currently have several useful methods as well.

    Do any of us have experience with what the best approach is?

  12. Bo says:

    My first implementation of math lib was using Float32Array, until I benchmarked it vs a object based implementation (i.e. vec3.x, vec3.y ,vec3.z). The object based implementation is a lot faster. I currently uses the object based implementation and only flatten it when setting mat4×4 GL uniforms.

  13. Bo says:

    See those benchmarks yourself: Float32Array based: http://jsperf.com/vecmath-matrix-library-test, Object based: http://jsperf.com/vecmath-matrix-library-test/2, Except matrix inverse, the other 3 operations are 7 times faster in object based implementation

  14. Shy says:

    making glMatrix work with javascript Arrays rather than Float32Arrays is as easy as commenting out one line of code. (Unless you have code which relies on it)

  15. Benoit Jacob says:

    A follow-up on my earlier remarks — the inverse transpose trick is actually good for the case where there is non-uniform scaling, contrary to what I believed. But it is uselessly expensive in the (common) case where there is no non-uniform scaling.

  16. giles says:

    Re: the normal matrix — for the tutorials, I’m going to stick to doing an inverse/transpose; what I really don’t want to do is give people code that stops working as soon as they start adding scaling or shearing matrices (thanks Benoit for the work isometric to describe what those matrices aren’t).

    However, inverting and so on the 4×4 matrices is, as Shy says, also broken. Now that Brandon Jones has added a mat3.transpose to glMatrix 0.9.5 (I think following a suggestion from Shy) I’ve updated the tutorials, and now use this, which I think is safe under all circumstances (and please let me know if it’s not!)


    var normalMatrix = mat3.create();
    mat4.toInverseMat3(mvMatrix, normalMatrix);
    mat3.transpose(normalMatrix);
    gl.uniformMatrix3fv(shaderProgram.nMatrixUniform, false, normalMatrix);

  17. Benoit Jacob says:

    There are two completely distinct things that the “inverse transpose” trick is used for:

    1) (legitimate part) on the 3×3 linear part of the transformation, it gives the corresponding transformation matrix to apply to normal vectors in the general case. That part is OK; it’s already quite expensive but not as expensive as doing it on a 4×4 matrix.

    2) (non legitimate part) on the whole 4×4 transformation matrix, it also plays the role of zeroing out the translation part (right column) by swapping it with the bottom row which *usually* starts with 0, 0, 0. That’s of course a very expensive and obfuscated way of zeroing out the translation part; it could also backfire if eventually it were applied to a general 4×4 matrix where the bottom row is not 0, 0, 0, 1.

    The good thing to do in general if you have a general affine transformation matrix, of the form

    l l l t
    l l l t
    l l l t
    0 0 0 1

    where the top-left 3×3 ‘l’ block is the linear part, is to take that top-left 3×3 ‘l’ block and to apply the “inverse transpose” method to it, and apply that to your 3D normal vectors.

    But there is never a good reason to compute the inverse of a 4×4 matrix here!

    Note that computing the inverse of a 3×3 matrix costs roughly 30 operations, while computing the inverse of a 4×4 matrix is roughly 200 operations!

  18. Benoit Jacob says:

    … then again, in many cases you only need this once per frame on the Modelview matrix, so speed doesn’t matter. But the Modelview matrix is almost always an isometry, so there is no need at all for “inverse transpose” in this case.

  19. Benoit Jacob says:

    Oh, and here’s the quickest proof I could find for why the inverse transpose trick (on the 3×3 linear part!) works. Take your 3×3 matrix M. Take a polar decomposition of M,

    M = R * S

    where R is a rotation and S is a scaling. By a ’scaling’ I mean a general scaling with positive or negative factors along any (mutually orthogonal) axes. This is equivalent to saying that S is a symmetric matrix (S == S^T). This is also equivalent to saying that dot(S*x, y) == dot(x, S*y) for any vectors x,y. This latter point will be very useful below.

    The inverse transpose of M is

    M.inverse().transpose() = R * S.inverse()

    Take any two mutually orthogonal vectors, x and n (n stands for ‘normal’). We have

    dot(M * x, M.inverse().transpose() * n)
    == dot(R * S * x, R * S.inverse() * n)
    == dot(S * x, S.inverse() * n) // because rotations don’t change dot
    == dot(x, S * S.inverse() * n) // because of above-mentioned property
    == dot(x, n)
    == 0

    Which is why using M.inverse().transpose() to transform normals works.

  20. Ramon says:

    ARRHHH

    I building a 6 degree of Freedom Camera here.
    and I stuck with the Gimbal lock
    and switching from one to another libraries doesn’t help me that much here.

    but I’ve also seen that there’s a Quaternion features build in Brandon’s library

  21. giles says:

    @Benoit — thanks for the explanations, very useful stuff. As you can see, the code I’ve wound up using with Shy’s guidance starts off by doing a mat4.toInverseMat3(mvMatrix, normalMatrix);. This means that the inverse is calculated on a 3×3 matrix rather than a 4×4, so there’s no extra work involved. You can see the source to confirm that.

  22. giles says:

    @Ramon — yup, that might help. Also, if you look at the way I coded the camera in lesson 11, I think it might be safe from gimbal lock — though I may be wrong.

  23. giles says:

    @Cedric — yup, that would be cool.

  24. Matte says:

    So what should I do to make my code capable of handling scaling and shearing? Should I normalize the normal vectors in the shaders after multiplying them by the normal matrix? Cause if not I think the effect coming out would not be correct right?

  25. giles says:

    @Matte — I’m still not sure that I understand all of the ins and outs of this, but I think that if you create the normal matrix the way I do in these tutorials, you don’t need to re-normalise them.

  26. Stephane says:

    @giles — I’ve tried your code on another test case (using an orthogonal projection for pixel perfect rendering), and it appears to not be resistant to the scaling transformation.
    If I apply a scaling transformation to my object, then the toInverseMat3 followed by the transpose trick doesn’t work, and I get no light on my faces.
    However it works perfectly with translation and rotations.
    Can you confirm if that the case for you (or maybe I did something wrong) ?

  27. Stephane says:

    I forgot to add that I am performing a scaling on 2 dimensions only (because of my orthogonal projection).

  28. Stephane says:

    OK, forget my last question, I was doing something wrong :)

    Just another question/remark:
    I wonder why you need to calculate the inverse of the rotation matrix before applying the transpose on it. The rotation matrix is orthogonal, so the transpose is also its inverse.
    Thus, the normals transformation should be OK, applying only the transpose of the top-left (3×3) model view matrix.

Leave a Reply

Subscribe to RSS Feed Follow Learning WebGL on Twitter