Quick Intro to Tangent Space – For 3D Artists

If you’re thinking “I’ve herd of Tangents before”, and you’ve not spent a lot of time animating or learning calculus, then there’s a good chance it’s from Tangent Space Normal Maps. In this short I’m going to take a quick look at tangent space, and relate it back to the Cross Product.

What is a Tangent?

A tangent by itself is usually represented as a straight line on a curve – normally on a graph – and represents the instantaneous rate of change at that point. For a more complete understanding of tangents, you’d need to spend some time learning introductory Calculus, which is not something I’d suggest many artists worry about unless there’s specific interest.

Once again, from Brilliant. C here is the tangent line of this graph, and the angle of it’s slope represents the rate of change. Steeper the slope, the faster the change.

Tangents also apply to 3D meshes, and with tangent space mapping, the tangent is the direction or “flow” of a mesh, representing largest rate of change in an meshes U co-ordinate in texture space.

I’m not going to go into computing the tangent space here, if you want to go down another rabbit hole, here’s a good thread from gamedev.stackexchange explaining directly. But you can think of the tangent as a “forward axis” along a mesh, with the bitangent being the “right” axis, and the normal being the “up” axis.

MikkTSpace and Tangent Space Normal Maps

The tangent vector is helpful as it gives gives us one of the two base vectors for our tangent space mapping. If we have two perpendicular directions in a 3D space, we can use the Cross Product to calculate the third. It’s common practise, using the MikkTSpace tangent conversion algorithm, covered in Morten Mikkelsen’s thesis here.

When encoding normal maps using Mikkelsen’s algorithm – which has almost become the standard for many 3D programs – only the normal and the tangent are stored. The bitangent, sometimes also known as the binormal, is computed from the Cross Product of the tangent and the normal when the mesh data is loaded by another software package.

The tangent, bitangent, and normal are in essence the co-ordinate system of texture space, like the XYZ of 3D space.

Tangent space normal maps adjust normals based on the surface tangent. Thus will look odd when used on a mesh with different surface tangents. This means that we cannot expect a normal map, baked with on mesh, to look right on a different mesh.

A quad mesh wireframe with two axis gizmos on the surface, representing the Tangent, Bitangent, and the Normal vectors of a surface.

Take this small quad, comprised of two triangles. On each triangle we can see the N or normal vector in green, pointing directly away from the triangle’s face. The T or tangent vector is in blue, pointing to the top left in this image, and the B or bitangent vector in red, pointing to the bottom left.

In the above static example, the TBN vectors just looked like our XYZ gizmos we’d normally see in a 3D DCC package – I’ve coloured the vectors the same so it’s a little more familiar.

However in the above animated clip you can see how the TBN vectors move and re-orient with the animated plane.

Because the tangent represents the direction of the mesh at a given point, if the tangent is incorrect, the normals from the normal map will be pointing in a different direction, and thus odd shading can occur.

Originally credited from the above thread as coming from Handplain 3D’s documentation but it’s now missing from their site. This image shows some of the shadowing you can see when tangents are incorrect.

Tangent space normal maps store normals based upon the original tangents of the mesh. A normal of (0,1,0) in a normal map doesn’t mean point up in world or object space, it means “Point directly away from the models surface”. So this tangent-space up vector would be an unchanged normal, as this is what the face normal would have been in the first place.

Normal Map Textures

A common method of storing normal maps in textures is the DXT5nm format. There are more specifics here if you’re really interested, but the two interesting element here are; normal maps often only store the x and y of the normal vector because of a quirk of how the DXT5 format keeps file sizes down. Because normal vectors are unit vectors, we can compute the z.

The other; as texture maps often only contain 0 - 1 values, and normals can point in negative directions, this value needs to be remapped to this range. So in a normal map, 0.5 is actually 0. This explains why normal maps tend to have a blueish colour as the x and y channels will generally be close to 0 (or 0.5), and the z will be closer to 1 as it’s commonly pointing away from the surface.

Once the normal map has been decoded, we have a unit vector per pixel, which in the fragment shader we’d transform into object space.

The TBN Matrix.

The tangent, bitangent, normal matrix, or TBN Matrix is what is the end destination for the tangent space vectors we’ve been looking at. When these are assembled in a matrix, we can then multiply our decoded normal vector by this matrix. This transforms it, from the flat (tangent) space of our normal map, to the object space of our mesh. In this case specifically, it just rotates our normal vector to our mesh’s surface.

Linear Transformations (Matrix Multiplication)

This is a fundamental, but still hard to approach concept of linear algebra. It’s also pretty foundational in 3D geometry and rendering. So I’ll very likely be paying it a visit some day here.

Like many of the math concepts I’ve been looking at, it can be boiled down to a node or function, and the trick for most will just be understanding when and how to use it. Mostly this is something like Shader Graph’s Transform node, which we can see from the documentation includes transforming from tangent space.

To Summarise

This short takes a quick fly-by of tangent space mapping as a supporting component of my Cross Product article. There’s much more to touch on, like linear transforms, but hopefully this provides the start of a foundation.