Normal Matrix Optimization

When it is time to render a mesh, we normally render that mesh multiplying all the vertices by a Model-View-Projection matrix. This matrix will move, rotate, and scale all the vertices; however, we don't want to do this for the normals associated with those vertices.

To correctly rotate normals we will need another matrix, normally called Normal Matrix. This matrix is a 3x3 matrix with a rotation encoded in it, and the short answer on how to get this matrix is to compute the transpose of the inverse of the Model matrix.

normal_mat = transpose(inverse(model_mat));

If you want to know more behind the math involved in this computation, please take a look at this fantastic post.

The only problem here is that inverse(...) operation is expensive :).

While trying to optimize our normal matrix computation, these are some characteristics that we were required to maintain (and others to take advantage of).

  • Our matrices are always orthogonal.
  • Our matrices can contain non-uniform scale.

At the same time, our goal is to compute a matrix that will rotate the normal associated with a vertex in the same way the model matrix does, but removing any non-uniform scale and any translation at the same time.

Lets put an example:

Model matrix

 X    Y    Z    
1.0  0.0  0.0  0.0
0.0  2.0  0.0  0.0
0.0  0.0  1.5  0.0
0.0  0.0  0.0  1.0

Normal matrix (expected)

 X    Y    Z    
1.0  0.0  0.0  0.0
0.0  0.5  0.0  0.0
0.0  0.0  0.66 0.0
0.0  0.0  0.0  1.0

In the example above we can see how the Y and Z axis have a non-uniform scale, and if we want to remove that scale, the axis itself will need to multiply by the inverse of the scale.

So, we can create a Normal Matrix with this idea in mind:

We extract the axis of the matrix to operate with them.

Matrix44 m = model_mat;

vec3 x = vec3(m[0][0], m[1][0], m[2][0]);
vec3 y = vec3(m[0][1], m[1][1], m[2][1]);
vec3 z = vec3(m[0][2], m[1][2], m[2][2]);

We compute the length of those axis.

float lx = length(x);
float ly = length(y);
float lz = length(z);

After we have the length, we will normalize them.

normalize(x);
normalize(y);
normalize(z);

We multiply them by the inverse of the length(scale) to remove any non-uniform scale on that axis.

x = x * (1.0 / lx);
y = y * (1.0 / ly);
z = z * (1.0 / lz);

We create a new Normal Matrix with the axis we have just computed.

Matrix33 normal_mat = identity_matrix;

normal_mat[0][0] = x[0];
normal_mat[1][0] = x[1];
normal_mat[2][0] = x[2];

normal_mat[0][1] = y[0];
normal_mat[1][1] = y[1];
normal_mat[2][1] = y[2];

normal_mat[0][2] = z[0];
normal_mat[1][2] = z[1];
normal_mat[2][2] = z[2];

At this point, normal_mat will contain a Normal Matrix that will be able to rotate normals and remove the non-uniform scale of them. This will be very important to allow a correct computation of the light.

This way of computing the Normal Matrix was ~55% faster than computing it by inverse(modela_mat).