OK, it looks like the author of Roaming Ralph was using some clever Matrix tricks, which he/she assumed that any person reading the program would be familiar with.

First, a brief clarification. The image you pasted in shows a 4x4 matrix as it is usually formatted in a math textbook, with the translation component of the matrix (dx, dy, dz) appearing in the fourth column:

[ 1, 0, 0, dx ]
[ 0, 1, 0, dy ]
[ 0, 0, 1, dz ]
[ 0, 0, 0, 1 ]

However, by Panda convention, we imagine that the sixteen components of the matrix are arranged differently, so that the translation component appears in the fourth row:

[ 1, 0, 0, 0 ]
[ 0, 1, 0, 0 ]
[ 0, 0, 1, 0 ]
[ dx, dy, dz, 1 ]

Note that is this a completely arbitrary convention, and doesn’t have anything to do with the way the matrix is actually laid out in memory or the way any computations are performed. But it does affect what you call a “row” or a “column”. The upshot of this is that, if we call mat.getRow3(3), we would return row 3–the last row–of the matrix expressed as a three-component vector, or exactly (dx, dy, dz): the translation component.

But that’s not what the code is doing. The code is calling mat.getRow3(1), which is row 1–the second row of the matrix. In a matrix with no rotation, as in the matrix above which represents only a translation, this will be the vector (0, 1, 0).

The magic requires knowing that when a matrix does contain a rotation, then the rotation is encoded in the first three rows of the matrix like this:

[ ix, iy, iz, 0 ]
[ jx, jy, jz, 0 ]
[ kx, ky, kz, 0 ]
[ 0, 0, 0, 1 ]

Where (ix, iy, iz) is the vector that represents the rotated X axis, and (jx, jy, jz) is the vector that represents the rotated Y axis, and (kx, ky, kz) is the vector that represents the rotated Z axis. For instance, for an unrotated matrix, these values are (1, 0, 0), (0, 1, 0), and (0, 0, 1), respectively–the unrotated X, Y, and Z axes.

But for any other matrix, mat.getRow3(1) returns the Y axis after rotation. Since Y axis is forward in Panda’s Z-up coordinate system, then mat.getRow3(1) returns the forward axis represented by this matrix.

Now the code does some other things. It zeroes out the Z component of this forward vector, because Ralph is only running in a plane. He might go up and down hills, but that’s handled separately; for the purposes of running via the controls, we only want to move him in X and Y. Now the original Y axis would have already been length 1 (unless the matrix had some scale in it), but once we zero out Z we might shorten the vector some–so we should re-normalize it to restore it to length 1. And then we scale the vector by a certain factor, according to how much time has elapsed since the last frame, and move Ralph by the resulting vector–moving him n feet forward per frame.

Why does the code call the Y axis “backward” instead of “forward”? Probably because Ralph is modeled to look down the -Y axis, instead of along the +Y axis, which is normally considered forward. So this code operates in Ralph’s world where -Y is forward and +Y is backward.

There’s no magic behind getNetTransform(). This is returning the accumulated transform by composing all matrices from render to Ralph–or his transform in global space. If Ralph’s parent doesn’t have a transform on it, and none of Rallph’s parents’ parents have any transforms on them, then Ralph is already in global space, and getNetTransform() is the same thing as getTransform(). Most simple applications are written where all nodes are in global space, but when you start to parent nodes under other nodes and thus inherit a transform from above, then it makes sense to make a distinction between local space, as returned by getTransform(), and global space, as returned by getNetTransform().


David, that was a fantastic explanation of a previously completely mysterious block of code. I just made use of it. Thanks.