This post is a follow-up to my 2006 ShaderX5 article [4] about normal mapping without a pre-computed tangent basis. In the time since then I have refined this technique with lessons learned in real life. For those unfamiliar with the topic, the motivation was to construct the tangent frame on the fly in the pixel shader, which ironically is the exact opposite of the motivation from [2]:

Since it is not 1997 anymore, doing the tangent space on-the-fly has some potential benefits, such as reduced complexity of asset tools, per-vertex bandwidth and storage, attribute interpolators, transform work for skinned meshes and last but not least, the possibility to apply normal maps to any procedurally generated texture coordinates or non-linear deformations.
Intermission: Tangents vs Cotangents
The way that normal mapping is traditionally defined is, as I think, flawed, and I would like to point this out with a simple C++ metaphor. Suppose we had a class for vectors, for example called Vector3, but we also had a different class for covectors. The latter are vectors that are made from the coefficients of a plane equation, and they transform differently than ordinary vectors. As you may know, normal vectors are an example of covectors. Suppose the class for covectors was called Covector3, having the same functionality as Vector3, but not compatible for assignment. Now imagine the following function:
Vector3 tangent; Vector3 bitangent; Covector3 normal; Covector3 perturb_normal( float a, float b, float c ) { return a * tangent + b * bitangent + c * normal; // ^^^^ compile-error: type mismatch for operator + } |
The above function mixes vectors and covectors in a single expression, which in this fictional example leads to a type mismatch error. If the normal is of type Covector3, then the tangent and the bitangent should be too, otherwise they cannot form a consistent frame, can they? In real life shader code of course, everything would be defined as float3 and be fine, or rather not.
Mathematical Compile Error
Unfortunately, the above mismatch is exactly how the ‘tangent frame’ for the purpose of normal mapping was introduced by the authors of [2]. This type mismatch is invisible as long as the tangent frame is orthogonal. When the exercise is however to reconstruct the tangent frame in the pixel shader, as this article is about, then we have to deal with a non-orthogonal screen projection. This is the reason why in the book I had introduced both
(which should be called co-tangent) and
(now it gets somewhat silly, it should be called co-bi-tangent) as covectors, otherwise the algorithm does not work. I have to admit that I could have been more articulate about this detail. This has caused real confusion, cf from gamedev.net:

The discrepancy is explained above, as my ‘tangent vectors’ are really covectors. The definition on page 132 is consistent with that of a covector, and so the frame
should be called a cotangent frame.
Intermission 2: Blinns Perturbed Normals (History Channel)
In this section I would like to show how the definition of
and
as covectors follows naturally from Blinns original bump mapping paper [1]. Blinn considers a curved parametric surface, for instance, a Bezier-patch, on which he defines tangent vectors
and
as the derivatives of the position
with respect to
and
.

In this context it is a convention to use subscripts as a shorthand for partial derivatives, so he is really saying
, etc. He also introduces the surface normal
and a bump height function
, which is used to displace the surface. In the end, he arrives at a formula for a first order approximation of the perturbed normal:
![]()
I would like to draw your attention towards the terms
and
. They are the perpendiculars to
and
in the tangent plane, and can be seen as the ‘offset vectors’ that ultimately displace the normal. They are also covectors (why, make the duck test: if it transforms like a covector, it is a covector) so adding them to the normal does not raise said type mismatch. Let’s divide this thing one more time by
and introduce
and
as follows:
![]()
![]()
where the hat (as in
) denotes the normalized normal.
can be interpreted as the normal to the plane of constant
, and likewise
as the normal to the plane of constant
. Therefore we have three normal vectors, or covectors,
,
and
, and they are the a basis of a cotangent frame. Equivalently,
and
are the gradients of
and
, which is the definition I had used in the book. The magnitude of the gradient therefore determines the bump strength, a fact that I will discuss later when it comes to scale invariance.
A Little Unlearning
The mistake of many authors is to unwittingly take
and
for
and
, which only works as long as the vectors are orthogonal. Let’s unlearn ‘tangent’, relearn ‘cotangent’, and repeat the historical development from this perspective: Peercy et al. [2] precomputes the values
and
(the change of bump height per change of texture coordinate) and stores them in a texture. They call it ‘normal map’, but is a really something like a ‘slope map’. (EDIT: Such maps have also become somewhat popular recently as derivative maps, but the general idea has been there for some time). Such a slope map cannot represent horizontal normals, as this would need an infinite slope to do so. It also needs some ‘bump scale factor’ stored somewhere as meta data. Kilgard [3] introduces the modern concept of a normal map as an encoded rotation operator, which does away with the approximation altogether, and instead gives the perturbed normal directly as
![]()
where the coefficients
,
and
are read from a texture. Most people would think that a normal map stores normals, but this is only superficially true. This idea of Kilgard was, since the unperturbed normal has coordinates
, it is sufficient to store the last column of the rotation matrix that would rotate the unperturbed normal to its perturbed position. So yes, a normal map stores basis vectors that correspond to perturbed normals. The difficulty starts to show up when normal maps are blended, since this is then an interpolation of rotation operators, with all the complexity that goes with it (for an excellent review, see the article about Reoriented Normal Mapping [5] here).
Solution of the Cotangent Frame
The problem to be solved for our purpose is the opposite as that of Blinn, the perturbed normal is known (from the normal map), but the cotangent frame is unknown. I’ll give a short revision of how I originally solved it. Define the unknown cotangents
and
as the gradients of the texture coordinates
and
as functions of position
, such that
![]()
where
is the dot product. The gradients are constant over the surface of an interpolated triangle, so introduce the edge differences
,
and
. The unknown cotangents have to satisfy the constraints

where
is the cross product. The first two rows follow from the definition, and the last row ensures that
and
have no component in the direction of the normal. The last row is needed otherwise the problem is underdetermined. It is straightforward then to express the solution in matrix form. For
,
![Rendered by QuickLaTeX.com \[\mathbf{T} = \begin{pmatrix} \Delta \mathbf{p_1} \\ \Delta \mathbf{p_2} \\ \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \end{pmatrix}^{-1} \begin{pmatrix} \Delta u_1 \\ \Delta u_2 \\ 0 \end{pmatrix} ,\]](http://www.thetenthplanet.de/wordpress/wp-content/ql-cache/quicklatex.com-c377936add32d6d88fd73e0a0a04d275_l3.png)
and analogously for
with
.
Into the Shader Code
The above result looks daunting, as it calls for a matrix inverse in every pixel in order to compute the cotangent frame! However, many symmetries can be exploited to make that almost disappear. Below is an example of a function written in GLSL to calculate the inverse of a 3×3 matrix. A similar function written in HLSL appeared in the book, and then I tried to optimize the hell out of it. Forget this approach as we are not going to need it at all. Just observe how the adjugate and the determinant can be made from cross products:
mat3 inverse3x3( mat3 M ) { // The original was written in HLSL, but this is GLSL, // therefore // - the array index selects columns, so M_t[0] is the // first row of M, etc. // - the mat3 constructor assembles columns, so // cross( M_t[1], M_t[2] ) becomes the first column // of the adjugate, etc. // - for the determinant, it does not matter whether it is // computed with M or with M_t; but using M_t makes it // easier to follow the derivation in the text mat3 M_t = transpose( M ); float det = dot( cross( M_t[0], M_t[1] ), M_t[2] ); mat3 adjugate = mat3( cross( M_t[1], M_t[2] ), cross( M_t[2], M_t[0] ), cross( M_t[0], M_t[1] ) ); return adjugate / det; } |
We can substitute the rows of the matrix from above into the code, then expand and simplify. This procedure results in a new expression for
. The determinant becomes
, and the adjugate can be written in terms of two new expressions, let’s call them
and
(with
as ‘perp’), which becomes
![Rendered by QuickLaTeX.com \[\mathbf{T} = \frac{1}{\left| \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \right|^2} \begin{pmatrix} \Delta \mathbf{p_2}_\perp \\ \Delta \mathbf{p_1}_\perp \\ \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \end{pmatrix}^\mathrm{T} \begin{pmatrix} \Delta u_1 \\ \Delta u_2 \\ 0 \end{pmatrix} ,\]](http://www.thetenthplanet.de/wordpress/wp-content/ql-cache/quicklatex.com-9fa2fa0fe1cec6d6c8f08ecf73d8f497_l3.png)
![]()
As you might guessed it,
and
are the perpendiculars to the triangle edges in the triangle plane. Say Hello! They are, again, covectors and form a proper basis for cotangent space. To simplify things further, observe:
- The last row of the matrix is irrelevant since it is multiplied with zero.
- The other matrix rows contain the perpendiculars (
and
), which after transposition just multiply with the texture edge differences. - The perpendiculars can use the interpolated vertex normal
instead of the face normal
, which is simpler and looks even nicer. - The determinant (the expression
) can be handled in a special way, which is explained below in the section about scale invariance.
Taken together, the opimized code is shown below, which is even simpler than the one I had originally published, but still higher quality:
mat3 cotangent_frame( vec3 N, vec3 p, vec2 uv ) { // get edge vectors of the pixel triangle vec3 dp1 = dFdx( p ); vec3 dp2 = dFdy( p ); vec2 duv1 = dFdx( uv ); vec2 duv2 = dFdy( uv ); // solve the linear system vec3 dp2perp = cross( dp2, N ); vec3 dp1perp = cross( N, dp1 ); vec3 T = dp2perp * duv1.x + dp1perp * duv2.x; vec3 B = dp2perp * duv1.y + dp1perp * duv2.y; // construct a scale-invariant frame float invmax = inversesqrt( max( dot(T,T), dot(B,B) ) ); return mat3( T * invmax, B * invmax, N ); } |
Scale invariance
The determinant
was left over as a scale factor in the above expression. This has the consequence that the resulting cotangents
and
are not scale invariant, but will vary inversely with the scale of the geometry. It is the natural consequence of them being gradients. If the scale of the geomtery increases, and everything else is left unchanged, then the change of texture coordinate per unit change of position gets smaller, which reduces
and similarly
in relation to
. The effect of all this is a diminished pertubation of the normal when the scale of the geometry is increased, as if a heightfield was stretched.
Obviously this behavior, while totally logical and correct, would limit the usefulness of normal maps to be applied on different scale geometry. My solution was and still is to ignore the determinant and just normalize
and
to whichever of them is largest, as seen in the code. This solution preserves the relative lengths of
and
, so that a skewed or stretched cotangent space is sill handled correctly, while having an overall scale invariance.
Non-perspective optimization
As the ultimate optimization, I also considered what happens when we can assume
and
. This means we have a right triangle and the perpendiculars fall on the triangle edges. In the pixel shader, this condition is true whenever the screen-projection of the surface is without perspective distortion. There is a nice figure demonstrating this fact in [4]. This optimization saves another two cross products, but in my opinion, the quality suffers heavily should there actually be a perspective distortion.
Putting it together
To make the post complete, I’ll show how the cotangent frame is actually used to perturb the interpolated vertex normal. The function perturb_normal does just that, using the backwards view vector for the vertex position (this is ok because only differences matter, and the eye position goes away in the difference as it is constant).
vec3 perturb_normal( vec3 N, vec3 V, vec2 texcoord ) { // assume N, the interpolated vertex normal and // V, the view vector (vertex to eye) vec3 map = texture2D( mapBump, texcoord ).xyz; #ifdef WITH_NORMALMAP_UNSIGNED map = map * 255./127. - 128./127.; #endif #ifdef WITH_NORMALMAP_2CHANNEL map.z = sqrt( 1. - dot( map.xy, map.xy ) ); #endif #ifdef WITH_NORMALMAP_GREEN_UP map.y = -map.y; #endif mat3 TBN = cotangent_frame( N, -V, texcoord ); return normalize( TBN * map ); } |
varying vec3 g_vertexnormal; varying vec3 g_viewvector; // camera pos - vertex pos varying vec2 g_texcoord; void main() { vec3 N = normalize( g_vertexnormal ); vec3 V = normalize( g_viewvector ); #ifdef WITH_NORMALMAP N = perturb_normal( N, V, g_texcoord ); #endif // ... } |
The green axis
Both OpenGL and DirectX place the texture coordinate origin at the start of the image pixel data. The texture coordinate (0,0) is in the corner of the pixel where the image data pointer points to. Contrast this to most 3-D modeling packages that place the texture coordinate origin at the lower left corner in the uv-unwrap view. Unless the image format is bottom-up, this means the texture coordinate origin is in the corner of the first pixel of the last image row. Quite a difference!
An image search on Google reveals that there is no dominant convention for the green channel in normal maps. Some have green pointing up and some have green pointing down. My artists prefer green pointing up for two reasons: It’s the format that 3ds Max expects for rendering, and it supposedly looks more natural with the ‘green illumination from above’, so this helps with eyeballing normal maps.
Sign Expansion
The sign expansion deserves a little elaboration because I try to use signed texture formats whenever possible. With the unsigned format, the value 0.5 cannot be represented exactly (it’s between 127 and 128). The signed format does not have this problem, but in exchange, has an ambiguous encoding for -1 (can be either -127 or -128). If the hardware is incapable of signed texture formats, I want to be able to pass it as an unsigned format and emulate the exact sign expansion in the shader. This is the origin of the seemingly odd values in the sign expansion.
In Hindsight
The original article in ShaderX5 was written as a proof-of-concept. Although the algorithm was tested and worked, it was a little expensive for that time. Fast forward to today and the picture has changed. I am now employing this algorithm in real-life projects for great benefit. I no longer bother with tangents as vertex attributes and all the associated complexity. For example, I don’t care whether the COLLADA exporter of Max or Maya (yes I’m relying on COLLADA these days) output usable tangents for skinned meshes, nor do I bother to import them, because I don’t need them! For the artists, it doesn’t occur to them that an aspect of the asset pipeline is missing, because It’s all natural: There is a geometry, there are texture coordinates and there is a normal map, and just works.
Take Away
There are no ‘tangent frames’ when it comes to normal mapping. A tangent frame which includes the normal is logically ill-formed. All there is are cotangent frames in disguise when the frame is orthogonal. When the frame is not orthogonal, then tangent frames will stop working. Use cotangent frames instead.
[1] James Blinn, “Simulation of wrinkled surfaces”, SIGGRAPH 1978
http://research.microsoft.com/pubs/73939/p286-blinn.pdf
[2] Mark Peercy, John Airey, Brian Cabral, “Efficient Bump Mapping Hardware”, SIGGRAPH 1997
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.42.4736
[3] Mark J Kilgard, “A Practical and Robust Bump-mapping Technique for Today’s GPUs”, GDC 2000
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.18.537
[4] Christian Schüler, “Normal Mapping without Precomputed Tangents”, ShaderX 5, Chapter 2.6, pp. 131 – 140
[5] Colin Barré-Brisebois and Stephen Hill, “Blending in Detail”,
http://blog.selfshadow.com/publications/blending-in-detail/
Out of interest, what implications does this have for tangent-space calculations when you *bake* normal-maps (for example, inside 3dsmax).
Wouldn’t you have to also use this method when creating the original normal-map?
Hi MoP, absolutely, you’re correct in your assumption.
If your normal map is the result of sampling high poly geometry, then the normal map sampler should use the same assumptions about tangent space than the in the engine, if you want highest fidelity. This is generally true, whether you use precomputed tangents or not.
If on the other hand you have a painted normal map, let’s say, a generic ripple texture, this just follows whatever texture mapping is applied to the mesh. If this mapping is sheared or stretched, and so violates the square patch assumption, the algorithm from above is still able to light it correctly.
Thanks for the fast reply, Christian!
Absolutely, that makes sense… so I’m wondering if, for “perfect” results, if people are baking their normal maps in 3dsmax or Maya or XNormal or whatever, those programs will need to have their tangent space calculations updated/reworked to match the in-game calculation?
I’m guessing that currently the 3dsmax calculation is not exactly synced with the method you describe here… or am I wrong?
You would be surprised to know that the method in 3dsMax or anywhere else is most likely not synced with any engine, even when using the classic method. There is an entire thesis devoted to this topic (look for Mikkelsen here). If you want such perfection, then the engine should have its own tools for baking normal maps because only then it is going to know what their shaders do. 3dsMax can never know this!
Yep, that makes sense - in fact that’s what the Doom 3 engine (id Tech 4) did to ensure the normal maps were perfect … they imported LWOs into the engine and baked the normal-maps using the game engine so that the calculations would match up perfectly.
I haven’t seen any other engine (released publically) do that yet, but I work in game development and I know that we have been trying to solve this problem for years with no satisfactory results. Currently the best method seems to be to modify a third-party program (eg. XNormal) to calculate tangent space in exactly the same way as the target engine calculates it, as this is the only way to ensure the normal-maps will be absolutely correct.
Pingback: Normal Mapping Without Precomputed Tangents « Interplay of Light
I am confused to what space your TBN matrix the normal from the normal map texture converts! You need this in order to do lighting calculations. Should I transform the light vector and view vector with the TBN matrix, or leave them in camera space?
Do you assume in the fragment shader that g_vertexnormal, g_viewvector are in the vertex shader multiplied with the gl_NormalMatrix and gl_ModelViewMatrix respectively? Otherwise how do you account for the transformations on the object.
Thanks
Sorry I meant in the previous post “light vector” and “eye vector” instead of view vector for the fist paragraph.
Hi cinepivates,
the TBN matrix that I build in the pixel shader is the transform from cotangent space to world space. After the vector from the normal map is multiplied with the TBN matrix, it is a world space normal vector. This is due to the fact that both the interpolated vertex normal and the view vector are supplied in world space by the vertex shader (not shown). Alternatively, if the vertex shader supplies these vectors in eye space, the pixel shader should construct the TBN matrix to convert into eye space instead. So you can choose your way.
Hi
thanks for the reply.
Since you are multiplying per pixel with a mat3 (TBNxNormal) on the pixel shader have you measured any performance decrease compared to the more traditional method of precomputed Tangents where you usually only transform in the vertex shader the light,eye vectors into tangent space using the TBN matrix.
I don’t use the TBN in the vertex shader. That is a thing of the past when there was pixel shader model 1.x. It prevents you to use world space constants in the pixel shader. For example, a reflection vector to look up a world space environment map. So the matrix multiply by TBN in the pixel shader has always been there for me.
Hi,I just wondering if lighting calculations can be done in tangent space.My textures could not match the Square Patch Assumption,but I don’t want to lose tangent space lighting,since some techniques are particular designed for that.
Hi April,
and
) will change under a non-orthogonal transformation. So
,
,
and any other vector you need into tangent space, and do the lighting computations there.
this is tricky, as lighting in tangent space does only work if it is orthogonal. Think about it: the angle between any vectors (say, the angle between
dot(N,L)is going to result in a different value, depending on which space it is in. If you can live with that, then take the inverse-transpose of the TBN matrix (this is in essence what you get when you do the non-perspective optimization mentioned in the post), and use that to transformThanks for reply
I managed to orthogonalize the matrix and then used the inverse-transpose one to do lighting in world space, the result became a mess.I checked the orgin matrix, and found out the matrix can not be inversed where UV mirrors. The determinant() function returns 0.
Still looking for reason.
I’m not sure I can follow your argument. If you want to do lighting in world space, then you don’t need to change anything. If you want to do lighting in tangent space instead (which only yields similar results if the tangent space is roughly orthogonal), you’d need a matrix to convert your light, view, etc vectors into tangent space. Edit: For this you need the inverse of the TBN. You can get the inverse-tranpose already very simply by ignoring the two cross products and using
dp2perp = dp1anddp1perp = dp2. Then the transpose of this would be the inverse of the TBN (ignoring scale). In the shader you don’t need to explicitly transpose, you can just multiply with the vectors from the left (egvector * matrixinstead oftranspose(matrix) * vector). Then you can orthogonalize this matrix if you want, but this won’t help much if the tangent space is not orthogonal to begin with. I don’t understand why you are willing to go though such hoops instead of simply doing the lighting in world space.Fantastic article! I switched my engine from using precalculated tangents to the method your describe - very easy to implement. I’m using WebGL, but I’m presuming my findings will correspond to what OpenGL + GLES programmers see. On desktop, I don’t really notice much of a performance difference (if anything, the tangent-less approach is slightly slower). But on mobile (tried on both Galaxy Nexus and Nexus 7), this method roughly twice as slow as using precomputed tangents. Seems dFdx and dFdy are particularly slow on mobile GPUs. Just cutting out those calls takes my FPS from ~24FPS to ~30FPS. So although I think this method is elegant to the extreme, I’m not sure it’s fast enough to be the better option (at least, not on mobile). I really hope someone can convince me otherwise though!
Hi Will, thanks for sharing. Of course those additional ~14 shader instructions for
cotangent_frame()are not free, especially not on mobile (which is like 2005 desktop, the time when the article was originally written). For me today, this cost is invisible compared to all the other things that are going on, like multiple lights and shadows and so forth. On the newest architectures like the NVidia Fermi, it could already be a performance win to go without precomputed tangents, due to the reasons mentioned in the introduction. But while that is nice, the main reason I use the method is the boost in productivity.Pingback: Martin Codes – Cool Link Stash, January 2013
For me the equations in this are screwed up. The are random LaTeX things in the images (Deltamathbf). Same with Safari, Chrome and Firefox.
Would be nice if you could fix that.
Something wrong with formulas. Maybe math plugin is broken?
Hallo there, I have fixed the math formulae in the post. It turned out to be an incompatibility between two plugins, and that got all
backslashes eaten. Sorry for that! Should there be a broken formula that I have overlooked, just drop a line.
So glad I found your blog. Though I am an artist these more technical insights really help me in understanding what to communicate to our coders to establish certain looks.
You’re welcome!
Hi, I want to ask you to elaborate on 2 following moments. First of all I would like to know why cross(N,Pv)/length(N)=gradient(u) and cross(Pu, N)/length(N)=gradient(v). I just can not find any mathematical derivation of this fact. Also it is unclear for me why deltaU = dot(T, deltaP), and how this follows from definition of gradient. Otherwise I found this article very exciting and my experiments showed that implementation works very well and is a good drop-in replacement for conventional tangent basis.
Hi Mykhailo,
, and that may not be obvious, so here it goes: The gradient vector is always perpendicular to the iso-surface (aka. “level-set”). In a skewed 2-D coordinate system, the iso-line of one coordinate is simply the other coordinate axis! So the gradient vector for the
texture coordinate must be perpendicular to the
axis, and vice versa. (The scale factor makes it such that the overall length equals the rate of change, which is dependent on the assumption that
.) Your second question is also related to the fact that the gradient vector is perpendicular to the iso-surface. If a position delta is made parallel to the iso-surface, then the texture coordinate doesn’t change, because in this case the dot product is zero.
thanks for sharing your experience. Indeed, I did not explain why
Thank you for an answer, now everything is a bit clearer. Still was able to fully figure it out only after I understood that parametrisation of u, v is linear in the plane of every triangle. For some reason I just missed that fact. And after you mentioned that gradient is perpendicular to iso-line everything made sense. And now it make sense why change of u is projection of position delta onto gradient vector.
Hi Christian, thanks for sharing!
I am working on an enhanced version of the Crytek-Sponza scene and I had problems with my per-pixel normalmapping shader based on precomputed tangents. I had most surfaces lighted correctly, but in some cases they weren’t. It turned out that - unknown to me - some faces had flipped UVs (which is not that uncommon) and therefore the cotangent frame was messed up, because the inverse of the tangent was used to calculate the binormal as the crossproduct with the vertex normal.
Now, I replaced it with your approach and everything is alright; it works instantly and all errors vanished. Awesome!!
Thanks
Hi,
Does the computation still work if the mesh normal is in view space, and the g_viewvector = vec3(0, 0, -1) ?
Cheers
Yes, but you must provide the vertex position in view space also. The view vector is used as a proxy to differentiate the vertex position, therefore a constant view vector will not do.
Ah ok, I just normalized the view position and its working perfectly
Thanks!
Hi,
Your idea seem to be very interesting.
But I’ve tried it in real scene, and found a glitch - if uv (texture coordinates) are mirrored, normal is also become mirrored.
Is there any way to fix it?
The normal itself should not be flipped, only the tangents. If the
texture coordinate is mirrored, then the
tangent should reverse sign and similar with
and the
tangent.