Vectors and Matrices
CS 381 Lecture, Dr. Lawlor
First, a pet peeve. The plural of "Matrix" is
"Matrices". The plural of "Vertex" is "Vertices". There is
no such word as "Matrice"; the singular is just "Matrix". There is no
such word as "Vertice"; just say "Vertex". English's foreign
words make no sense. For some reason I can deal with "Matrixes",
although it's hard to say. "Vertice"
Ok, so you've got a point in 3D space. You're representing that
point with 3 floating-point numbers, call them (x,y,z). We
usually refer to the whole bundle of 3 coordinates as a "vector", and
think of it as a single thing. Virtually everything you'll do in
computer graphics requires mathematically operating on vectors.
Vector Operations
The graphics hardware prefers dealing with 4-component vectors, called
"vec4"s in GLSL. So I can set up a vec4 to represent a point
(x,y,z) like this:
vec4 v = vec4(x, y, z, 1.0);
Warning: the usual C++ style initialization "vec4 v(x, y, z, 1.0);"
will give a rather unhelpful "syntax error parse error". Just
keep reminding yourself that GLSL isn't C++...
The 4'th component of your vectors, "w", is usually 1.0.
You can pull out the components of a vector using a period like "v.x", which is just the vector v's x coordinate.
Dot Product
The coolest vector operation is "dot product". The dot product
just multiplies corresponding coordinates of the vector, and then adds
the multiplied coordinates together. You could write a version of
dot product in GLSL (or C++) like this:
float my_dot(vec4 a,vec4 b) {
return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
}
But there's a faster (single clockcycle!) builtin routine called
"dot(a,b)" that does the same thing. So for example, in the
previous lecture when we did isometric projections and wrote:
gl_Position = vec4(
/* output X_w */ +0.7*v.x +0.0*v.y -0.6*v.z -0.3*v.w,
...);
we could have equivalently written this faster code:
const vec4 make_x = vec4(+0.7,+0.0,-0.6,-0.3);
gl_Position = vec4(
/* output X_w */ dot(make_x,v),
...);
The "const" puts the variable into read-only memory on the graphics card, which speeds up the program a bit.
Dot product has all sorts of nice properties:
- dot(a,b) = dot(b,a) "Commutative"
- dot(a,b+c) = dot(a,b)+dot(a,c) "Distributes over addition"
- dot(a,k*b) = k*dot(a,b) = dot(k*a,b) "Distributes over scalings"
Matrix Operations
Sadly there are two different, but entirely equivalent ways to think about matrix arithmetic for graphics.
Vectors as Columns (Good)
With column vectors, you think of transforming a 2D homogenous vector
by a matrix by multiplying the matrix by the vector, like this:
[ s ] [ a b c ][ x ]
[ t ] = [ d e f ][ y ]
[ 1 ] [ 0 0 1 ][ 1 ]
We do the matrix-vector multiply by taking dot products of matrix rows with our input vector. Here's a silly animation of matrix-vector product using column vectors. Note the bottom row of the transform matrix is 0 0 1 (to make sure the output W coordinate is still 1).
Most mathematics (including the Math 314 textbook used at UAF) use the good column-vector format. The OpenGL documentation
uses the good column-vector format. Graphics textbooks have
mostly come around to the good format. The Angel book Appendix C
describes both formats. In this class, I will always use good
column vectors, and never evil row vectors.
Vectors as Rows (Evil)
With evil row vectors, you think of transforming a 2D homogenous vector
by a matrix by multiplying the vector by the matrix, like this:
[ a d 0 ]
[ s t 1 ] = [ x y 1 ] [ b e 0 ]
[ c f 1 ]
Note now the rightmost column of the transform matrix is 0 0 1. We do the vector-matrix multiply by taking dot products of the matrix columns with our input vector.
Most older graphics textbooks and papers use evil row vectors.
DirectX uses evil row vectors (the few times it comes up).
OpenGL's "glLoadMatrixf"
routine, and GLSL's "mat4" matrix element order looks suspiciously like
the evil row vector version, even though the documentation describes it
as good column vectors.
Other descriptions of this controversy: on usenet, on hplus.
When This Doesn't Really Matter
Note in both cases, s=a*x + b*y +c and t=d*x+e*y+f.
So the bottom-line floating-point operations are exactly the same in
both cases. The incoming X axis projects to (a,d); the Y axis to
(b,e) ; and the origin to (c,f) in both cases. The hardware
doesn't deal with matrices in any case, but instead uses individual
vectors.
When This Does Matter
GLSL's matrix load is carefully calculated to make nobody happy.
When you're building a matrix in GLSL, it looks a lot like the evil
row-vector version, and the documentation doesn't actually say where
the elements go. But once you've loaded the matrix, you can use
it for good or evil. In reality, you're just specifying the values in
the columns of the matrix, which only looks funny when you write them
out in rows:
/* Compact but confusing way to write GLSL matrix initialization */
mat4 m = mat4(
a, d, 0.0, 0.0, /* <- column 0 of matrix */
b, e, 0.0, 0.0, /* <- column 1 of matrix */
0.0,0.0,0.0, 0.0, /* <- column 2 of matrix */
c, f, ,0.0,1.0, /* <- column 3 of matrix */
);
/* Long but clearer way to write the same GLSL matrix initialization */
mat4 m = mat4(
a, /* <- top of column 0 of matrix */
d,
0.0,
0.0,
b, /* <- top of column 1 of matrix */
e,
0.0,
0.0,
0.0, /* <- top of column 2 of matrix (all zeros) */
0.0,
0.0,
0.0,
c, /* <- top of column 3 of matrix */
f,
0.0,
1.0
);
Often, you won't hardcode matrices into GLSL, but upload them from the C++ program using glUniformMatrix4fv,
which luckily takes a "transpose" parameter you probably want to set to
GL_TRUE--this lets you upload the matrix in the usual left-to-right,
row-by-row order.
To use any matrix in GLSL with good column vectors, you just multiply
the matrix by the vector, like "m*v". To use the same matrix with
evil row vectors, multiply the vector by the matrix, like "v*m".
In GLSL "m[i]" returns the i'th column of the matrix, as a column
vector. As a bonus, this syntax appears to be less likely to
randomly crap out for big matrices (due to driver bugs) than the above
16-floats syntax.