Every now and then a situation arises when geometry must be generated or at least manipulated with custom code in such a way that the tangent-space coordinate system defined at each vertex must be calculated from scratch. If you assign such a task to a junior or mid-level programmer it’s likely they won’t have a clue how to do this and will end up doing a copy-paste-refactor job on a piece of code found on the internet. Down the road someone will notice that the bumps on your cloth or ripples on your water are inverted, rotated or just “wrong” according to some hand-wavy explanation of “weirdness” by the troubled author of a once beautiful normal-map. Chances are the original piece of code from the internet had some sort of transform implicit in the calculations that corrected an incompatibility between the tangent-space coordinate system and the object-space coordinate system. Perhaps the handedness changed from left to right or the z-axis became the y-axis and vice-versa or any other subtle yet crucial detail. If no one knows how tangent-spaces are derived fixing this is a pain and will inevitably lead to some poor soul trying a seemingly endless permutation of transposed matrices, swapped vector components, inverted axis directions and any other easily made mathematical perturbation until the results finally look correct.

To solve this problem like proper professionals we need an intuitive understanding of what tangent-space is. In words it’s the coordinate system that the normals in your normal map are defined in. To visualize this imagine a texture with a red arrow extending from the center that points to the left and a similarly located green arrow that points down; now mentally paste this texture onto a triangle in object-space.

If you were to measure the delta between the beginning and end of the red arrow you would get a vector that is parallel to the x-axis of the tangent-space expressed in object-space coordinates (this is called the tangent). Apply the same method to the green arrow and you end up with a vector that is parallel to the y-axis (this is called the binormal). The z-axis of the tangent-space is simply the triangle’s outward facing surface normal. As a short hand we will call the un-normalized version of these vectors ** i**,

**, and**

*j***respectively. To build a rotation matrix that transforms directions from tangent-space to object-space simply normalize**

*k***,**

*i***, and**

*j***and drop them into the first, second and third column of a 3×3 matrix.**

*k*Now that we have a mental model of what needs to be calculated we can get down to hammering out the mathematics. Suppose we had the following function:

That takes a point (*u*, *v*) in texture-space as input and produces a point (*x*, *y*, *z*) in object-space in such a way that is consistent with the texture mapping of our imaginary triangle; in other words, for any point (*u*, *v*) on our texture ** p**(

*u*,

*v*) tells us where in object-space that point is. Going back to the visual analogy used before we can use this function to measure the delta between the object-space start and end points of the red arrow (corresponding to the tangent vector or

**) and the green arrow (corresponding to the binormal vectors or**

*i***) on the texture.**

*j*Note that our choice for a starting (*u*, *v*) is irrelevant since left is always left and down is always down regardless of where we are on the texture. If you are Calculus savvy Equations 2.1 and 2.2 should look suspiciously similar to the numerical approximations to the partial derivatives of ** p** in the

*u*and

*v*direction; indeed, they are equivalent.

The trick is coming up with the function ** p**(

*u*,

*v*) so that we can compute it’s derivatives, this is actually fairly simple. Suppose our triangle has the following object-space vertices:

and texture-space vertices (a.k.a. uv’s):

We can parameterize object-space and texture-space points on the triangle with the following vector equations:

Where (*s*, *t*) pairs that sum to a value on [0, 1] produce points on the triangle in texture-space or object-space. We want to find the partial derivatives of *x*, *y* and *z* with respect to *u* and *v*. To do this we need to be able to write *x*, *y* and *z* as a function of *u* and *v*, not *s* and *t* as we did in Equation 4.2. Equation 4.1 maps pairs of *s* and *t* to pairs of *u* and *v* so we start by inverting it such that we can do the mapping in reverse.

Now we can rewrite Equation 4.2 in terms of *u* and *v*.

Alas our arbitrary (*s*, *t*) parameters go away and we are left with a function that maps texture coordinates to object-space. Finding the tangent-space basis vectors ** i** and

*j*is a simple matter of expanding Equation 6.0 and evaluating the partial derivatives. Obviously this gets quite cumbersome given the mess of subscripts and subtractions. To clean things up we’ll adopt the following equalities:

Substituting into Equation 6.0 yields:

And differentiating both sides with respect to *u* and *v* leaves us with:

There you have it, ** i** and

**as a function of texture-space and object-space edge vectors.**

*j*Just for fun here’s some c++ code you can attempt to copy and paste into your own project. Is it bug free? Perhaps. I can assure you that it is not caveat free.

struct Vertex { Vector3 vPos; // object-space position Vector2 vTex; // texture-space position Vector3 vTangent; // Tangent space x-axis (left on the texture) Vector3 vBinormal; // Tangent space y-axis (down on the texture) Vector3 vNormal; // Tangent space z-axis (out of the texture) void SumBasis(const Vector3& a_vAxisI, const Vector3& a_vAxisJ, const Vector3& a_vAxisK) { vTangent += a_vAxisI; vBinormal += a_vAxisJ; vNormal += a_vAxisK; } void NormalizeBasis(void) { vTangent.Normalize(); vBinormal.Normalize(); vNormal.Normalize(); } void ZeroBasis(void) { vTangent = Vector3::vZeroVector; vBinormal = Vector3::vZeroVector; vNormal = Vector3::vZeroVector; } }; Matrix2x2 CalcTangentMatrix(Vector2& a_vTexA, Vector2& a_vTexB, Vector2& a_vTexC) { Matrix2x2 mtx; mtx.e11 = a_vTexB.x - a_vTexA.x; mtx.e12 = a_vTexC.x - a_vTexA.x; mtx.e21 = a_vTexB.y - a_vTexA.y; mtx.e22 = a_vTexC.y - a_vTexA.y; mtx.Invert(); return mtx; } void SumTangentSpace(Vertex& a_kVertexA, Vertex& a_kVertexB, Vertex& a_kVertexC) { // Compute object space edge vectors Vector3 vEdgeAB = a_kVertexB.vPos - a_kVertexA.vPos; Vector3 vEdgeAC = a_kVertexC.vPos - a_kVertexA.vPos; // Compute the triangle's normal vector and area Vector3 vNormal = Vector3::CrossProduct(vEdgeAB, vEdgeAC); float32 fArea2x = vNormal.Magnitude(); // Compute the tangent matrix Matrix2x2 mtxTex = CalcTangentMatrix(a_kVertexA.vTex, a_kVertexB.vTex, a_kVertexC.vTex); // Compute tangent-space basis vectors Vector3 vAxisI = vEdgeAB * mtxTex.e11 + vEdgeAC * mtxTex.e12; Vector3 vAxisJ = vEdgeAB * mtxTex.e21 + vEdgeAB * mtxTex.e22; // Normalize and weigh each vector by the area of the triangle vAxisI = Vector3::Normalize(vAxisI) * fArea2x; vAxisJ = Vector3::Normalize(vAxisJ) * fArea2x; // Accumulate the tangent space basis vector at each vertex a_kVertexA.SumBasis(vAxisI, vAxisJ, vNormal); a_kVertexB.SumBasis(vAxisI, vAxisJ, vNormal); a_kVertexC.SumBasis(vAxisI, vAxisJ, vNormal); } void CalcTangentSpace(const int32* a_pIndexList, int32 a_nNumIndices, Vertex* a_pVertexList, int32 a_nNumVertices) { for(int32 i = 0; i < a_nNumVertices; i++) a_pVertexList[i].ZeroBasis(); for(int32 i = 0; i < a_nNumIndices; i+=3) { int32 nIndexA = a_pIndexList[i]; int32 nIndexB = a_pIndexList[i+1]; int32 nIndexC = a_pIndexList[i+2]; SumTangentSpace(a_pVertexList[nIndexA], a_pVertexList[nIndexB], a_pVertexList[nIndexC]); } for(int32 i = 0; i < a_nNumVertices; i++) a_pVertexList[i].NormalizeBasis(); return; }