How do I build my tangent space matrix?

I'd like to understand how the process of building the "tangent space" matrix for normal mapping works. I'm following several tutorials, and other stackexchange questions, but I'm unfamiliar with the math involved, and a lot of resources seem to be telling me "just do this and it'll work".

According to the LearnOpenGL tutorial, you should build your tangent space matrix with the following code:

vec3 T = normalize(vec3(model * vec4(aTangent,   0.0)));
vec3 B = normalize(vec3(model * vec4(aBitangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(aNormal,    0.0)));
mat3 TBN = mat3(T, B, N)

The first three lines are variations of:

vec3 worldSpaceVec = normalize(vec3(modelMatrix * vec4(modelSpaceVec, 0)));

Other implementations I've seen do:

vec3 worldSpaceVec = normalize(mat3(modelMatrix) * modelSpaceVec);

(by the way, I'm not sure I visualize why the LearnOpenGL tutorial doesn't use vec4(modelSpaceVec, 1); I'm not sure what either value would represent or how it would change the results. I assume this just a property of normals that we always represent them with a w of 0, but I don't get why it applies to the tangent and bitangent)

But more importantly, I don't understand the modelMatrix * ... part of the equation. According to this answer, normals are bivectors, a different type of data which is represented by a row matrix instead of a column matrix (that is, bivectors are $\begin{bmatrix} x & y & z \end{bmatrix}$ where vectors are$\begin{bmatrix} x \\ y \\ z \end{bmatrix}$).

If my understanding is correct, then normals are of a different type than regular vectors, just like kilograms and liters are similar but different types; and they need to be treated differently in regard to transformations. So why does the tutorial use:

vec3 worldSpaceVec = normalize(vec3(modelMatrix * vec4(modelSpaceVec, 0)));

and not

vec3 worldSpaceVec = inverse(transpose(modelMatrix)) * modelSpaceVec;

or even

vec3 worldSpaceVec = inverse(transpose(mvpMatrix)) * modelSpaceVec;

?

In other words, should the tangent and bitangent vectors of a vertex be converted from one space to another using the same transformation matrix as the vertex's normal vector, and if so, why?

• FYI The w=0 part is for all directions. Stuff that you don't want to have translation affect. – ratchet freak May 3 '18 at 8:36

In Normal Mapping, you want to go to "Tangent Space", that is the space where the center of the coordinate system is your Vertex (/Fragment). Just like you go from Model Space to World Space and from World Space to Eye Space, you can go to Tangent Space with a matrix multiplication.

In Tangent Space, the vector $n_T=\left(\begin{array}{c}0\\0\\1\end{array}\right)$ is always the Normal - since you read the normal $n_N$ out of the Normal Map in your Fragment Shader, you will obviously not use $n_T$ but $n_N$ for lighting calculations and this choice of $n_N$ rather than $n_T$ brings about the nice effect normal mapping has.

What's a bit special is that every vertex has its own Tangent Space, which is why you need the tangent, bitangent$^1$ and (geometry) normal of every vertex to construct your TBN matrix.

So much for theory - since you didn't really ask about that specifically, I assume you understand this (otherwise just edit your question).

by the way, I'm not sure I visualize why the LearnOpenGL tutorial doesn't use vec4(modelSpaceVec, 1); I'm not sure what either value would represent or how it would change the results. I assume this just a property of normals that we always represent them with a w of 0, but I don't get why it applies to the tangent and bitangent

Vectors in general are represented with a $0$ in the $w$ coordinate. If you look into a translation matrix, you will notice that the last row is the one that moves points around. For vectors, this is not a wanted behaviour - a vector is the same, no matter where, since it is a direction. So to avoid "translating" a vector, you set the $w$ coordinate to $0$. Since your tangent and bitangent vectors are vectors, the $w$ coordinate is $0$ here as well.

But more importantly, I don't understand the modelMatrix * ... part of the equation.

Using the the model matrix $M$ at this point is correct in most cases - but not all of them. The normal matrix $N$ is (as the answer you linked correctly said) the inverse transpose of the model matrix. But, since normals are vectors, you don't need a 4x4 matrix, a 3x3 matrix is just fine. And if you leave out that translation part, and you scale the same amount in all three coordinates, then the transpose of your model matrix is the same as the inverse of your model matrix. Therefore you get:

$N = (M^{-1})^T = (M^T)^T = M$

And that is why in most cases (i.e. everytime you use the same scaling in every coordinate) the normal matrix is the same as the model matrix.

vec3 worldSpaceVec = inverse(transpose(modelMatrix)) * modelSpaceVec;

This is correct, however

vec3 worldSpaceVec = inverse(transpose(mvpMatrix)) * modelSpaceVec;

this is not correct. Once you have transformed your normals into world space, you need to use the same view and projection matrix as before. Your view matrix brings your viewer into the origin (or rather it moves everything such that your viewer is the origin), inverse transposing this doesn't make sense (and similarly it doesn't make sense for the projection matrix. plus, the order in matrix multiplications is important, which is the next problem... don't do it ;-) )

Now, since you know that the model matrix may be the same as the normal matrix, you can (mathematically correctly) just go with the inverse transpose of the model matrix. Obviously this costs quite a lot of performance, especially inverting a matrix is costly. It is therefore more efficient to just go with the model matrix.

In other words, should the tangent and bitangent vectors of a vertex be converted from one space to another using the same transformation matrix as the vertex's normal vector, and if so, why?

Unlike normals, tangent and bitangent vectors should be transformed with the model matrix, independently of the whether the normal matrix equals the modal matrix. A normal is always supposed to be perpendicular to a surface, and that's why simply taking the model matrix may be incorrect - in some cases the normal matrix would not be perpendicular to the surface anymore. Tangents however are always "lying" on the surface, so if you rotate a surface, the tangent must be rotated the same way to still "lie" on the surface.

$^1$ Sometimes this is called binormal, however Eric Lengyel explains why this is an incorrect term in Mathematics for 3D Game Programming and Computer Graphics.

• "since you didn't really ask about that specifically, I assume you understand this" I do, but the added context is nice :) Thanks for this very thorough answer! Just to make sure I understood, you're saying that the only case where the inverse transpose isn't redundant is when, say, a mesh is being rescaled on one axis but not the others? Follow up question: if I do want to calculate the normals in camera space (for instance, to avoid floating point rounding errors), how do I do it? Would using inverse(transpose(modelViewMatrix)) be enough? – Narrateur du chaos May 3 '18 at 7:13
• he inverse transpose isn't redundant is when, say, a mesh is being rescaled on one axis but not the others? there is also the shearing transformation and i am not sure about that, but i doubt it leads to normal matrix = model matrix. but in general, that's the most likely case to brake equality. if I do want to calculate the normals in camera space you can do lighting in world space - you will need to transform your lighting and viewing vectors to world space and then to tangent space, but otherwise it doesn't matter.still, the view matrix part is wrong,it's just the modelMatrix you want – Tare May 3 '18 at 7:17