Tangent Space Derivation

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.

Tangent Space Figure 00

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, j, and k respectively.  To build a rotation matrix that transforms directions from tangent-space to object-space simply normalize i, j, and k and drop them into the first, second and third column of a 3×3 matrix.

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:

Tangent Space Figure 01

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 i) and the green arrow (corresponding to the binormal vectors or j) on the texture.

Tangent Space Figure 02

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.

Tangent Space Figure 03

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:

Tangent Space Figure 04

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

Tangent Space Figure 05

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

Tangent Space Figure 06

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.

Tangent Space Figure 07

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

Tangent Space Figure 08

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:

Tangent Space Figure 09

Substituting into Equation 6.0 yields:

Tangent Space Figure 10

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

Tangent Space Figure 11

There you have it, i and j as a function of texture-space and object-space edge vectors.

Tangent Space Figure 12

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;
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s