CS 5620: Computer Graphics

These two classes implement an n-dimensional vector and nxm-dimensional matrix as static arrays. Keep in mind that this design decision and others leads to a few tradeoffs.

  • Functions that should be returning a vector or matrix whose size is dependent upon function arguments actually return a vector or matrix whose size is the same as the original. The correct values are placed in the first elements and the rest are initialized with zero. A static cast operator is provided so that you may truncate these values if need be.
  • Neither class is zero initialized upon creation using the default constructor. This conflicted with some of the later functionality. Many times it also saves you from unecessary instructions, however it can lead to a headache later. Just be careful of this.
  • Because these classes are templated, creating vectors or matricies of difference sizes and types duplicates the vector or matrix class in your code for each different size and type pair. This can lead to large executable sizes.
  • Since the arrays are static, both of these classes may be used in buffers to be sent to OpenGL.
  • Allocating a vector or matrix takes less time using static arrays instead of dynamic because you don't have to wait for memory allocation.

That said, lets get on to using them.

Vector

This vector class implements a column vector of any size and data type.

Declaration and Initialization

Creating a vector is as easy as the following:

vec<float, 4> x;
vec<float, 4> y(1, 2, 4, 1);
vec<float, 4> z(y);

Keep in mind that when vectors are declared with the default constructor, all of the elements of the array are left uninitialized. Also, there are quite a few typedefs provided that make things a little cleaner. These typedefs may be found in geometry.h. The following code does the same as the above:

vec4f x;
vec4f y(1, 2, 4, 1);
vec4f z(y);

Once a vector is declared, you may set or access its values in a few different ways

vec4f x;

// set all four elements of x to 0
x.fill(0);

// set elements 1 and 2 to 5 and 4 respectively
x.set(1, 3, vec2f(5, 4));
cout << x << endl;

// set element 3 to 6
x[3] = 6;
cout << x << endl;

// print a subvector of x with elements 1, 2, and 3
cout << x(1, 4) << endl;

// swap elements 1 and 2
x.swap(1, 2);
cout << x << endl;

Keep in mind that the subvector operator returns a vector whose size is equal to the original. In order to get a vector with the right size, you will need to truncate it with a static cast.

vec4f x(1, 2, 3, 4);
cout << (vec3f)x(1,4) << endl;

Add, Subtract, Multiply, Divide

These basic operators are defined for vector-vector, vector-constant, and constant-vector operands.

vec4f v0(1, 2, 4, 1);
vec4f v1(2, 5, 2, 1);

cout << (v0 + v1) << endl;
cout << (v0 - v1) << endl;
cout << (v0 * v1) << endl;
cout << (v0 / v1) << endl;
cout << (v0 + 3.0f) << endl;
cout << (v0 - 3.0f) << endl;
cout << (v0 * 3.0f) << endl;
cout << (v0 / 3.0f) << endl;
cout << (3.0f + v1) << endl;
cout << (3.0f - v1) << endl;
cout << (3.0f * v1) << endl;
cout << (3.0f / v1) << endl;

Vector-vector operators work on a per element basis. When adding and subtracting vectors, the size of the resulting vector is the max of the sizes of the two operands. When you multiply or divide vectors, the size of the resulting vector is the min of the sizes of the two operands. Keep in mind that when operating between constants and vectors, the type of the constant must match the type of the vector.

Vector Math

The library also provides basic vector math functionality.

vec4f x(1, -2, 2, 1), y(2, 1, 2, 1);

// return a vector whose elements are the absolute values of the input vector
cout << abs(x) << endl;

// normalize the vector by dividing by its magnitude
cout << norm(x) << endl;

// Return the 3D cross product of these two vectors
cout << cross(vec3f(1, 2, 3), vec3f(3, 2, 1)) << endl;

// Return the 3D cross product of these two homogenious vectors
cout << cross(vec4f(1, 2, 3, 4), vec4f(4, 3, 2, 1)) << endl;

// Return the magnitude of the vector
cout << mag(x) << endl;

// Return the squared magnitude of the vector
cout << mag2(x) << endl;

// Return the dot product of the two vectors
cout << dot(x, y) << endl;

// Return the euclidean distance between the two vectors
cout << dist(x, y) << endl;

// Return the squared euclidean distance between the two vectors
cout << dist2(x, y) << endl;

// Return a normalized vector representing the direction from x to y
cout << dir(x, y) << endl;

// Clamp all the element of x to between 0 and 1
cout << clamp(x, 0.0f, 1.0f) << endl;

// make a 3D homogenious vector using the first three elements of the input vector
cout << homogenize(vec6f(1, 2, 3, 4, 5, 6)) << endl;

Matrix

This matrix class implements a matrix of any height, width, and data type.

Declaration and Initialization

The following all declare matrices.

// Declare an uninitialized matrix of height 4 and width 2
mat<float, 4, 2> m0;

// Declare an initialized matrix of height 4 and width 2
mat<float, 4, 2> m2(1, 2,
2, 1,
2, 1,
1, 2);

// Declare an initialized matrix of height 2 and width 4
mat<float, 2, 4> m3(1, 2, 2, 1,
2, 1, 1, 2);
mat<float, 2, 4> m4(m2);
cout << m2 << endl;
cout << m3 << endl;

Once again, keep in mind that the default constructor leaves all of the elements uninitialized, and that the typedefs in geometry.h make life easier.

// Declare an initialized matrix of height 4 and width 4
mat4f m(1, 2, 2, 1,
3, 4, 5, 6,
2, 6, 3, 2,
1, 6, 3, 2);
cout << m << endl;

Once a matrix is declared, you may set or access its values in a few different ways

mat4f m(11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44);

// Set row 2 to [1, 2, 3, 4]
m[2] = vec4f(1, 2, 3, 4);

// Return row 2
cout << m[2] << endl << endl;

// Set row 2 to [4, 3, 2, 1]
m.set_row(2, vec4f(4, 3, 2, 1));

// Return row 2
cout << m.row(2) << endl << endl;

// Set row 1 to [1, 2, 3, 4] and set row 2 to [5, 6, 7, 8]
m.set_row(1, 3, mat<float, 2, 4>(1, 2, 3, 4, 5, 6, 7, 8));

// Return a submatrix that consists of rows 1 and 2
cout << m.row(1, 3) << endl;

// Set column 1 to [1, 2, 3, 4]
m.set_col(1, vec4f(1, 2, 3, 4));

// return column 1
cout << m.col(1) << endl << endl;

// Set column 1 to [1, 2, 3, 4] and set column 2 to [5, 6, 7, 8]
m.set_col(1, 3, mat<float, 2, 4>(1, 2, 3, 4, 5, 6, 7, 8));

// return a submatrix consisting of columns 1 and 2
cout << m.col(1, 3) << endl;

// Set the submatrix of rows 1 and 2 and columns 2 and 3 to the matrix [[1 2][3 4]]
m.set(1, 3, 2, 4, mat2f(1, 2, 3, 4));
cout << m << endl;

// Return the submatrix of rows 1 and 2 and columns 2, and 3
cout << m(1, 3, 2, 4) << endl;

// remove row 1 and column 2
cout << m.remove(1, 2) << endl;

// swap rows 1 and 2
m.swapr(1, 2);

// swap columns 1 and 2
m.swapc(1, 2);
cout << m << endl;

Once again, keep in mind that most of these sub-matrix operators actually return a matrix of the original size. If you want a matrix of the correct size, you will have to do a static cast.

mat4f m(11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44);

cout << (mat2f)m(1, 3, 2, 4) << endl;

Add, Subtract, Multiply, Divide

These basic operators are defined for matrix-matrix, matrix-constant, and constant-matrix operands.

mat4f m0(1, 2, 4, 1, 6, 5, 3, 5, 2, 1, 2, 5, 1, 2, 1, 1);
mat4f m1(2, 5, 2, 1, 2, 4, 5, 3, 1, 2, 4, 2, 1, 2, 5, 1);

cout << (m0 + m1) << endl;
cout << (m0 - m1) << endl;
cout << (m0 * m1) << endl;
cout << (m0 / m1) << endl;
cout << (m0 + 3.0f) << endl;
cout << (m0 - 3.0f) << endl;
cout << (m0 * 3.0f) << endl;
cout << (m0 / 3.0f) << endl;
cout << (3.0f + m1) << endl;
cout << (3.0f - m1) << endl;
cout << (3.0f * m1) << endl;
cout << (3.0f / m1) << endl;

matrix-vector multiplication and vector-matrix division are also defined.

mat4f m(1, 2, 4, 1, 6, 5, 3, 5, 2, 1, 2, 5, 1, 2, 1, 1);
vec4f v(1, 2, 3, 4);

cout << (m*v) << endl;
cout << (v/m) << endl;

Matrix Math

There is also basic fuctionality for the various special functions.

mat4f m(3, 4, 2, 4, 2, 4, 1, 4, 1, 3, 2, 5, 3, 1, 2, 3);

cout << trace(m) << endl;
cout << determinant(m) << endl;
cout << adjugate(m) << endl;
cout << transpose(m) << endl;

// Return an identity matrix of type and size determined by the template parameters <type, height, width>
cout << identity<float, 4, 4>() << endl;

// Return the reduced row eshelon form of this matrix
cout << rref(m) << endl;
cout << inverse(m) << endl;
cout << invertible(m) << endl;
cout << rank(m) << endl;

Keep in mind that the inverse function always returns a result. However if the input matrix is not invertible, this result will be wrong. So when inverting a matrix you either need to know that it is invertible or check that it is using the invertible function.