A (longwinded!) derivation of tangent space basis for triangular meshes for normal-mapping
In various newsgroups and message boards I have quite often seen the question asking how to derive the per-vertex tangent space vectors for a triangular mesh in order to perform normal mapping. Even more often, a question asking for code to do this. I’ve never found the explanations on the web particularly satisfactory, although the problem is very simple to solve – maybe that’s why people don’t bother with careful explanations. Anyway, I thought I might as well write up an explanation here, and include some code too.
If you want to email me about this, use nashn at tcd dot ie.
Derivation for a 1 dimensional surface
Firstly, the desire to estimate the tangents is, of course, really a desire to estimate the partial derivatives of the surface at each vertex on the surface. To do this we make various assumptions about the surface. The approach described here is to just (i) Compute the tangents along each face (triangle for a triangular mesh) of the surface, and (ii) Estimate the tangent at a vertex by averaging the tangents of all faces which the vertex belongs to.
A triangle mesh forms a 2D surface, which we display in 3D. Instead of finding the tangent space for a 2D surface, it’s easier to solve for a 1D surface first. In the 1D case, our triangle mesh becomes a set of line segments. Here’s a little diagram:
B D
/ \ / \
/ \ / \
/ \ / \
A C E
Here A, B, C, D, E are vertices, and the connectivity of our surface is defined with { AB, BC, CD, DE }.
We want to estimate per-vertex tangent vectors, an easy way to do this is to find the tangents along each line-segment and then average each consecutive pair. So here we’d compute the tangent along AB, which is just B - A, and along BC, which is just C - B and then compute ((B - A) + (C - B)) / 2. This gives us an estimated tangent at vertex B.
So it seems the problem is solved for a 1D surface: Just go through each line segment as dictated by the connectivity information between the vertices and compute a vector along it. Then go through each vertex and just average its two neighbouring tangents.
The simple solution doesn’t work in general though. A perfectly valid description of our surface above would have been, to leave the vertices exactly, where they are. But, to change the connectivity information to { AB, CB, CD, DE }. Now this would still render fine. The problem is, the averaged tangent at B will be ((B - A) + (B - C)) / 2. There are two choices for the vector along each line segment, in this case we’ve swapped convention when moving from segment AB to segment CB. Our averaging is making some kind of assumption about the vectors along each segment varying smoothly, this swap in convention makes that wrong.
So, how can we solve this? Since both surfaces were represented in a perfectly valid way for rendering. Both were *correct* descriptions of the desired surface. We need to understand what we want from our tangents. Consider flattening out the surface along a line, doing some (different) transformation to each line-segment. Here’s what I mean in a diagram, just to be completely clear:
B D
/ \ / \ per-segment transformation
/ \ / \ ----------------------------->
/ \ / \
A C E A----B----C----D----E
Now the tangents along the segments of the flattened surface should all point the same way. If we apply the inverse of those per-segment transformations to the tangent (there is only one, since the tangent along each segment is the same), then we get the desired tangents along each segment of the original surface.
Now comes the trick. If the surface is texture-mapped, then the vectors in the texture map, formed by pairs of texture coordinates, are a pre-flattened out version of our surface! For each segment, we just need to transform the tangent (it only has one, since it’s flat) to the texture-map so that it lies along that segment. Luckily, if the surface is texture-mapped we can use the texture map coordinates to solve the problem.
V W X Y Z *---*---*---*---* Texture . . . . . . B . D . . / \ . / \ . . / \ . / \ . Surface ./ \./ \. A C E
So by looking at the diagram directly above, let’s put our per-segment transformation plan into practise. This diagram shows the texture mapped onto the surface, the dotted lines show the correspondance between the vertices and the texture coordinates. If we conceptualise our texture as a 1 dimensional vector space, then it has one basis vector, call it T. The mapping of the texture coordinates onto the vertex coordinates defines a mapping of this basis vector T ”along” the surface at each line segment. Using this mapped T vector as a tangent will never cause a change in tangent convention like was described above.
Now, the last thing is that our T vector exists in a 1D space, whereas the tangents along our surface expressed as vectors, exist in a 2D space. So the mapping of T along the surface will be from 1D to 2D. Moreover due to the nature of texture mapping it will take the form of a linear transformation. So, from the diagram above, the vector W - V, will be mapped onto the vector B - A. So we can write this as matrices:
[ a ] [ Bx - Ax ] [ b ] [ W - V ] = [ By - Ay ]
a and b here are the entries of the matrix for our linear transformation from 1D to 2D. Bx and Ax denote the x coordinates of B and A respectively. It’s very easy to solve for a and b here, they are just:
a = (Bx - Ax) / (W - V) b = (By - Ay) / (W - V)
Now we can use this same matrix to transform the vector T. T can be anything we choose, 1 is probably a good choice. Multipling our matrix above with the matrix [1] just gives us that matrix back again, Giving our tangent, T at each segment as:
T = 1 / (W - V) * (B - A)
These tangents all obey the same ”convention” and can be safely averaged. Solving the problem for a 2D surface is done the same way.
Derivation for a 2 dimensional surface
We can take exactly the same approach to solve this as we did above. This time the texture-map vectors form a flattened out version of our mesh, or at least, it should, otherwise this approach is wrong.
We have a linear transformation from R^2 to R^3 this time. If we have a triangle with vertex coordinates (P, Q, R) and texture coordinates (u1, u2, u3), then we can express the action of our linear transformation on the vectors that correspond to each other between each space. So if the matrix of our linear transformation is M, then we have:
M * (u2 - u1) = Q - P M * (u3 - u1) = R - P Denoting u1 = (u1u, u1v) and similarly for u2, u3 and also filling in the entries of M gives: [ a b ] [ Qx - Px ] [ c d ] [ u2u - u1u ] = [ Qy - Py ] [ e f ] [ u2v - u1v ] [ Qz - Pz ] and also [ a b ] [ Rx - Px ] [ c d ] [ u3u - u1u ] = [ Ry - Py ] [ e f ] [ u3v - u1v ] [ Rz - Pz ]
Now we can bash out these matrix multiplies and then regroup a bit, for example, multiplying out the first row of each matrix with the second one gives:
a * (u2u - u1u) + b * (u2v - u1v) = Qx - Px for the first one, and for the second one we get: a * (u3u - u1u) + b * (u3v - u1v) = Rx - Px
Note that we get exactly the same thing for the next two rows - just change a, b for c, d and Qx - Px and Rx - Px to Qy - Py and Ry - Py. We can do the same change to get the third row. The result of this is that we can re-write the equations matrix form again:
[ u2u - u1u u2v - u1v ] [ a ] [ Qx - Px ] [ u3u - u1u u3v - u1v ] [ b ] = [ Rx - Px ]
Denoting u2u - u1u and u12u, u2v - u1v as u12v, and similarly for u3u - u1u we can solve for a, b by inverting the 2x2 matrix, giving:
[ a ] [ u13v -u12v ] [ Qx - Px ] [ b ] = 1 / (u12u * u13v - u13u * u12v) * [ -u13u u12u ] [ Rx - Px ]
And we can solve for c, d by just changing the Qx - Px and Rx - Px to Qy - Py and Ry - Py. And we can solve for e, f by changing them to Qz - Pz and Rz - Pz. The result of this is that we can express the first column of the linear transformation’s matrix, i.e. a, c, e as:
1 / det * u13v * (Q - P) - u12v * (R - P) and the second column, i.e. b, d, f as: 1 / det * -u13u * (Q - P) + u12u * (R - P) Where det = u12u * u13v - u13u * u12v
So finally, having determined what our linear transformation is, we just need to apply it to the basis vectors of our texture, called them V1 and V2. They can be anything we want, but should be perpendicular. A good choice is:
V1 = [ 1 ]
[ 0 ]
and V2 = [ 0 ]
[ 1 ]
Now
[ a b ] [ a ]
[ c d ] [ 1 ] = [ c ]
[ e f ] [ 0 ] [ e ]
and V2's transformation is just the other column
So actually, the tangent space vectors along our triangle are just:
T = 1 / det * u13v * (Q - P) - u12v * (R - P) B = 1 / det * -u13u * (Q - P) + u12u * (R - P)
T and B stand for Tangent and Binormal here. Or, some people say the correct term is not Binormal but Bitangent. As the normal we can either use the geometric normal (derived from averaging face normals) or we can compute T cross B (or B cross T, depending on which points in the desired direction). We usually normalise these vectors so that T, B and N form an orthonormal basis for our 3d scene, this means that we can invert the change-of-basis matrix consisting of T, B, N by just finding its transpose, allowing us to transform vectors into or out of tangent space easily.
Now we must of course average these to get the tangent space estimation at vertices.
Source Code
// v is the array of vertices
// uv is the array of texture coordinates
// indices defines the topology of the surface
// numTris is the number of triangles in the mesh
// T, B are pre-allocated arrays of floats used to hold
// the components of the tangent and binormal vectors at each vertex
// Hacked together by Nicholas Nash - free to use in anything noncommercial.
void calcTangentSpace(const VecArray& v, const TexCoordArray& uv, unsigned int *indices, int numTris, float *T, float *B)
{
int len = v.length;
Vec3 *s = new Vec3[len];
Vec3 *t = new Vec3[len];
int *triCount = new int[len];
int i, j = 0;
for(i = 0; i < numTris; i++)
{
int v0 = indices[j];
int v1 = indices[j + 1];
int v2 = indices[j + 2];
float u13u = uv[v2].u - uv[v0].u;
float u13v = uv[v2].v - uv[v0].v;
float u12u = uv[v1].u - uv[v0].u;
float u12v = uv[v1].v - uv[v0].v;
Vec3 v1v2 = v[v1] - v[v0];
Vec3 v1v3 = v[v2] - v[v0];
float det_inv = 1.0f / (u12u * u13v - u13u * u12v);
Vec3 p = det_inv * (u13v * v1v2 - u12v * v1v3);
Vec3 q = det_inv * (u12u * v1v3 - u13u * v1v2);
s[v0] += p;
t[v0] += q;
s[v1] += p;
t[v1] += q;
s[v2] += p;
t[v2] += q;
j += 3;
triCount[v0]++;
triCount[v1]++;
triCount[v2]++;
}
for(i = 0; i < len; i++)
{
if(triCount[i])
{
s[i] /= triCount[i];
t[i] /= triCount[i];
}
s[i].normalise();
t[i].normalise();
T[i * 3] = t[i].x;
T[i * 3 + 1] = t[i].y;
T[i * 3 + 2] = t[i].z;
B[i * 3] = s[i].x;
B[i * 3 + 1] = s[i].y;
B[i * 3 + 2] = s[i].z;
}
delete [] s;
delete [] t;
delete [] triCount;
return;
}