Tangent Space Sep 22, 2006
Looking at the 3dkingdoms.com server logs, I saw that that sometimes people would end up on my normal mapping tutorial by searching for "tangent space normal mapping." Since that tutorial is strictly object space, it doesn't contain any tangent space implementation details, so I decided to write some up here. Since I do use object space for many of my complex meshes, I may not be as famaliar with tangent space issues as I would be otherwise.
Overview
What are the tangent and bitangent vectors used in tangent space normal mapping? A plane's normal is perpendicular to the plane, while the tangent and bitangent are parallel to the plane. Their direction is determined by the UV coordinates, one points in the direction of U-axis in 3d space, the other in the direction of the V-axis. The tangent space normal map stores in each component the distance along each of these vectors. For example, one component of the normal map (usually stored in the blue part) points directly away from the mesh triangle, ie. along the triangle normal. This allows the same normal map to be reused on different parts of a mesh. Calculating the Tangent Basis
The function below computes the tangent and bitangent (also refered to as binormal) vectors for a single triangle. The triangle vertex positions are P1, P2, P3, the vertex UV coordinates are UV1, UV2, UV3 and the function writes tangent and bitangent.
void ComputeTangentBasis( const Vec3& P1, const Vec3& P2, const Vec3& P3, const Vec2& UV1, const Vec2& UV2, const Vec2& UV3, Vec3 &tangent, Vec3 &bitangent ) { Vec3 Edge1 = P2 - P1; Vec3 Edge2 = P3 - P1; Vec2 Edge1uv = UV2 - UV1; Vec2 Edge2uv = UV3 - UV1; float cp = Edge1uv.y * Edge2uv.x - Edge1uv.x * Edge2uv.y; if ( cp != 0.0f ) { float mul = 1.0f / cp; tangent = (Edge1 * -Edge2uv.y + Edge2 * Edge1uv.y) * mul; bitangent = (Edge1 * -Edge2uv.x + Edge2 * Edge1uv.x) * mul; tangent.Normalize(); bitangent.Normalize(); } }Since I'm normalizing anyway, the only thing 1.0f/cp can do is flip the vectors depending on the winding of the triangle UV coordinates, so these steps could be redundant. You will have to normalize the vectors eventually, but it could be done after the smoothing step described in the next section.
While the divide by cp isn't important for making a rotation matrix of unit vectors, it does scale the tangent and bitangent to be Blending the Tangent/Bitangent Vectors
Let's say you are creating smoothed vertex normals, by adding the face normals of adjacent triangles for each vertex, then normalizing the vector. You can do the same thing with tangent and bitangent vectors. Note that the 3 vectors (normal, tangent, bitangent) may not form an orthogonal basis since the tangent & bitangent directions are based on texture uv coordinates and possibly averaged from multiple vectors. What you can do is use Gram-Schmidt orthogonalization. This is performed simply by subtracting the part of a vector in the direction of another vector then renormalizing. For example the code to make the tangent orthogonal to the normal is: tangent -= normal * tangent.dot( normal ); tangent.normalize();Once you have two orthogonal vectors you can use the cross product to compute the 3rd. The orthogonalization step seems to be mainly necessary on complex curved models or places with texture seems. Many texture seems however will cause weird lighting no matter what, so if the tangent/bitangent vectors are too far apart you'll probably be better off leaving a split at the seem and not smoothing them together. Using Tangent Space with Shaders
To use tangent space normal mapping, we need to send the tangent and bitangent vectors to the vertex shader. Then we can create a 3x3 rotation matrix from them that can be used to rotate vectors such as the light vector and half-angle vector into tangent space.
float3x3 objToTangentSpace = float3x3( IN.Tangent, IN.Bitangent, IN.Normal ); OUT.halfVec = mul(objToTangentSpace, halfVec ); OUT.lightVec = mul(objToTangentSpace, lightVec);Now we can dot these vectors with a normal from our tangent space normal map, using the same pixel shader as with object space normal mapping. |