Followup: Normal Mapping Without Precomputed Tangents

This post is a follow-​up to my 2006 ShaderX5 arti­cle [4] about nor­mal map­ping with­out a pre-​computed tan­gent basis. In the time since then I have refined this tech­nique with lessons learned in real life. For those unfa­mil­iar with the topic, the moti­va­tion was to con­struct the tan­gent frame on the fly in the pixel shader, which iron­i­cally is the exact oppo­site of the moti­va­tion from [2]:

Since it is not 1997 any­more, doing the tan­gent space on-​the-​fly has some poten­tial ben­e­fits, such as reduced com­plex­ity of asset tools, per-​vertex band­width and stor­age, attribute inter­po­la­tors, trans­form work for skinned meshes and last but not least, the pos­si­bil­ity to apply nor­mal maps to any pro­ce­du­rally gen­er­ated tex­ture coor­di­nates or non-​linear deformations.

Inter­mis­sion: Tan­gents vs Cotangents

The way that nor­mal map­ping is tra­di­tion­ally defined is, as I think, flawed, and I would like to point this out with a sim­ple C++ metaphor. Sup­pose we had a class for vec­tors, for exam­ple called Vector3, but we also had a dif­fer­ent class for cov­ec­tors. The lat­ter are vec­tors that are made from the coef­fi­cients of a plane equa­tion, and they trans­form dif­fer­ently than ordi­nary vec­tors. As you may know, nor­mal vec­tors are an exam­ple of cov­ec­tors. Sup­pose the class for cov­ec­tors was called Covector3, hav­ing the same func­tion­al­ity as Vector3, but not com­pat­i­ble for assign­ment. Now imag­ine the fol­low­ing 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 func­tion mixes vec­tors and cov­ec­tors in a sin­gle expres­sion, which in this fic­tional exam­ple leads to a type mis­match error. If the normal is of type Covector3, then the tangent and the bitangent should be too, oth­er­wise they can­not form a con­sis­tent frame, can they? In real life shader code of course, every­thing would be defined as float3 and be fine, or rather not.

Math­e­mat­i­cal Com­pile Error

Unfor­tu­nately, the above mis­match is exactly how the ‘tan­gent frame’ for the pur­pose of nor­mal map­ping was intro­duced by the authors of [2]. This type mis­match is invis­i­ble as long as the tan­gent frame is orthog­o­nal. When the exer­cise is how­ever to recon­struct the tan­gent frame in the pixel shader, as this arti­cle is about, then we have to deal with a non-​orthogonal screen pro­jec­tion. This is the rea­son why in the book I had intro­duced both \mathbf{T} (which should be called co-​tangent) and \mathbf{B} (now it gets some­what silly, it should be called co-​bi-​tangent) as cov­ec­tors, oth­er­wise the algo­rithm does not work. I have to admit that I could have been more artic­u­late about this detail. This has caused real con­fu­sion, cf from gamedev.net:

The dis­crep­ancy is explained above, as my ‘tan­gent vec­tors’ are really cov­ec­tors. The def­i­n­i­tion on page 132 is con­sis­tent with that of a cov­ec­tor, and so the frame \left(\mathbf{T}|\mathbf{B}|\mathbf{N}\right) should be called a cotan­gent frame.

Inter­mis­sion 2: Blinns Per­turbed Nor­mals (His­tory Channel)

In this sec­tion I would like to show how the def­i­n­i­tion of \mathbf{T} and \mathbf{B} as cov­ec­tors fol­lows nat­u­rally from Blinns orig­i­nal bump map­ping paper [1]. Blinn con­sid­ers a curved para­met­ric sur­face, for instance, a Bezier-​patch, on which he defines tan­gent vec­tors \mathbf{p}_u and \mathbf{p}_v as the deriv­a­tives of the posi­tion \mathbf{p} with respect to u and v.

In this con­text it is a con­ven­tion to use sub­scripts as a short­hand for par­tial deriv­a­tives, so he is really say­ing \mathbf{p}_u = \partial \mathbf{p} / \partial u, etc. He also intro­duces the sur­face nor­mal \mathbf{N} = \mathbf{p}_u \times \mathbf{p}_v and a bump height func­tion f, which is used to dis­place the sur­face. In the end, he arrives at a for­mula for a first order approx­i­ma­tion of the per­turbed normal:

 \[\mathbf{N}' \simeq \mathbf{N} + \frac{f_u \mathbf{N} \times \mathbf{p}_v + f_v \mathbf{p}_u \times \mathbf{N}}{|\mathbf{N}|} ,\]

I would like to draw your atten­tion towards the terms \mathbf{N} \times \mathbf{p}_v and \mathbf{p}_u \times \mathbf{N}. They are the per­pen­dic­u­lars to \mathbf{p}_u and \mathbf{p}_v in the tan­gent plane, and can be seen as the ‘off­set vec­tors’ that ulti­mately dis­place the nor­mal. They are also cov­ec­tors (why, make the duck test: if it trans­forms like a cov­ec­tor, it is a cov­ec­tor) so adding them to the nor­mal does not raise said type mis­match. Let’s divide this thing one more time by |\mathbf{N}| and intro­duce \mathbf{T} and \mathbf{B} as follows:

 \begin{align*} \mathbf{T} &= \frac{\mathbf{N} \times \mathbf{p}_v}{|\mathbf{N}|^2} = \nabla u, & \mathbf{B} &= \frac{\mathbf{p}_u \times \mathbf{N}}{|\mathbf{N}|^2} = \nabla v, \end{align*}

 \[\mathbf{N}' \simeq \widehat{\mathbf{N}} + f_u \mathbf{T} + f_v \mathbf{B} ,\]

where the hat (as in \widehat{\mathbf{N}}) denotes the nor­mal­ized nor­mal. \mathbf{T} can be inter­preted as the nor­mal to the plane of con­stant u, and like­wise \mathbf{B} as the nor­mal to the plane of con­stant v. There­fore we have three nor­mal vec­tors, or cov­ec­tors, \mathbf{T}, \mathbf{B} and \mathbf{N}, and they are the a basis of a cotan­gent frame. Equiv­a­lently, \mathbf{T} and \mathbf{B} are the gra­di­ents of u and v, which is the def­i­n­i­tion I had used in the book. The mag­ni­tude of the gra­di­ent there­fore deter­mines the bump strength, a fact that I will dis­cuss later when it comes to scale invariance.

A Lit­tle Unlearning

The mis­take of many authors is to unwit­tingly take \mathbf{T} and \mathbf{B} for \mathbf{p}_u and \mathbf{p}_v, which only works as long as the vec­tors are orthog­o­nal. Let’s unlearn ‘tan­gent’, relearn ‘cotan­gent’, and repeat the his­tor­i­cal devel­op­ment from this per­spec­tive: Peercy et al. [2] pre­com­putes the val­ues f_u and f_v (the change of bump height per change of tex­ture coor­di­nate) and stores them in a tex­ture. They call it ‘nor­mal map’, but is a really some­thing like a ‘slope map’. (EDIT: Such maps have also become some­what pop­u­lar recently as deriv­a­tive maps, but the gen­eral idea has been there for some time). Such a slope map can­not rep­re­sent hor­i­zon­tal nor­mals, as this would need an infi­nite slope to do so. It also needs some ‘bump scale fac­tor’ stored some­where as meta data. Kil­gard [3] intro­duces the mod­ern con­cept of a nor­mal map as an encoded rota­tion oper­a­tor, which does away with the approx­i­ma­tion alto­gether, and instead gives the per­turbed nor­mal directly as

 \[\mathbf{N}' = a \mathbf{T} + b \mathbf{B} + c \widehat{\mathbf{N}} ,\]

where the coef­fi­cients a, b and c are read from a tex­ture. Most peo­ple would think that a nor­mal map stores nor­mals, but this is only super­fi­cially true. This idea of Kil­gard was, since the unper­turbed nor­mal has coor­di­nates (0,0,1), it is suf­fi­cient to store the last col­umn of the rota­tion matrix that would rotate the unper­turbed nor­mal to its per­turbed posi­tion. So yes, a nor­mal map stores basis vec­tors that cor­re­spond to per­turbed nor­mals. The dif­fi­culty starts to show up when nor­mal maps are blended, since this is then an inter­po­la­tion of rota­tion oper­a­tors, with all the com­plex­ity that goes with it (for an excel­lent review, see the arti­cle about Reori­ented Nor­mal Map­ping [5] here).

Solu­tion of the Cotan­gent Frame

The prob­lem to be solved for our pur­pose is the oppo­site as that of Blinn, the per­turbed nor­mal is known (from the nor­mal map), but the cotan­gent frame is unknown. I’ll give a short revi­sion of how I orig­i­nally solved it. Define the unknown cotan­gents \mathbf{T} = \nabla u and \mathbf{B} = \nabla v as the gra­di­ents of the tex­ture coor­di­nates u and v as func­tions of posi­tion \mathbf{p}, such that

 \begin{align*} \mathrm{d} u &= \mathbf{T} \cdot \mathrm{d} \mathbf{p} , & \mathrm{d} v &= \mathbf{B} \cdot \mathrm{d} \mathbf{p} , \end{align*}

where \cdot is the dot prod­uct. The gra­di­ents are con­stant over the sur­face of an inter­po­lated tri­an­gle, so intro­duce the edge dif­fer­ences \Delta u_{1,2}, \Delta v_{1,2} and \Delta \mathbf{p_{1,2}}. The unknown cotan­gents have to sat­isfy the constraints

 \begin{align*} \Delta u_1 &= \mathbf{T} \cdot \Delta \mathbf{p_1} , & \Delta v_1 &= \mathbf{B} \cdot \Delta \mathbf{p_1} , \\ \Delta u_2 &= \mathbf{T} \cdot \Delta \mathbf{p_2} , & \Delta v_2 &= \mathbf{B} \cdot \Delta \mathbf{p_2} , \\ 0 &= \mathbf{T} \cdot \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} , & 0 &= \mathbf{B} \cdot \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} , \end{align*}

where \times is the cross prod­uct. The first two rows fol­low from the def­i­n­i­tion, and the last row ensures that \mathbf{T} and \mathbf{B} have no com­po­nent in the direc­tion of the nor­mal. The last row is needed oth­er­wise the prob­lem is under­de­ter­mined. It is straight­for­ward then to express the solu­tion in matrix form. For \mathbf{T},

 \[\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} ,\]

and anal­o­gously for \mathbf{B} with \Delta v.

Into the Shader Code

The above result looks daunt­ing, as it calls for a matrix inverse in every pixel in order to com­pute the cotan­gent frame! How­ever, many sym­me­tries can be exploited to make that almost dis­ap­pear. Below is an exam­ple of a func­tion writ­ten in GLSL to cal­cu­late the inverse of a 3×3 matrix. A sim­i­lar func­tion writ­ten in HLSL appeared in the book, and then I tried to opti­mize the hell out of it. For­get this approach as we are not going to need it at all. Just observe how the adju­gate and the deter­mi­nant 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 sub­sti­tute the rows of the matrix from above into the code, then expand and sim­plify. This pro­ce­dure results in a new expres­sion for \mathbf{T}. The deter­mi­nant becomes \left| \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \right|^2, and the adju­gate can be writ­ten in terms of two new expres­sions, let’s call them \Delta \mathbf{p_1}_\perp and \Delta \mathbf{p_2}_\perp (with \perp as ‘perp’), which becomes

 \[\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} ,\]

 \begin{align*} \Delta \mathbf{p_2}_\perp &= \Delta \mathbf{p_2} \times \left( \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \right) , \\ \Delta \mathbf{p_1}_\perp &= \left( \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \right) \times \Delta \mathbf{p_1} . \end{align*}

As you might guessed it, \Delta \mathbf{p_1}_\perp and \Delta \mathbf{p_2}_\perp are the per­pen­dic­u­lars to the tri­an­gle edges in the tri­an­gle plane. Say Hello! They are, again, cov­ec­tors and form a proper basis for cotan­gent space. To sim­plify things fur­ther, observe:

  • The last row of the matrix is irrel­e­vant since it is mul­ti­plied with zero.
  • The other matrix rows con­tain the per­pen­dic­u­lars (\Delta \mathbf{p_1}_\perp and \Delta \mathbf{p_2}_\perp), which after trans­po­si­tion just mul­ti­ply with the tex­ture edge differences.
  • The per­pen­dic­u­lars can use the inter­po­lated ver­tex nor­mal \mathbf{N} instead of the face nor­mal \Delta \mathbf{p_1} \times \Delta \mathbf{p_2}, which is sim­pler and looks even nicer.
  • The deter­mi­nant (the expres­sion \left| \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \right|^2) can be han­dled in a spe­cial way, which is explained below in the sec­tion about scale invariance.

Taken together, the opimized code is shown below, which is even sim­pler than the one I had orig­i­nally pub­lished, 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 invari­ance

The deter­mi­nant \left| \Delta \mathbf{p_1} \times \Delta \mathbf{p_2} \right|^2 was left over as a scale fac­tor in the above expres­sion. This has the con­se­quence that the result­ing cotan­gents \mathbf{T} and \mathbf{B} are not scale invari­ant, but will vary inversely with the scale of the geom­e­try. It is the nat­ural con­se­quence of them being gra­di­ents. If the scale of the geomtery increases, and every­thing else is left unchanged, then the change of tex­ture coor­di­nate per unit change of posi­tion gets smaller, which reduces \mathbf{T} = \nabla u = \left( \frac{\partial u}{\partial x}, \frac{\partial u}{\partial y}, \frac{\partial u}{\partial z} \right) and sim­i­larly \mathbf{B} in rela­tion to \mathbf{N}. The effect of all this is a dimin­ished per­tu­ba­tion of the nor­mal when the scale of the geom­e­try is increased, as if a height­field was stretched.

Obvi­ously this behav­ior, while totally log­i­cal and cor­rect, would limit the use­ful­ness of nor­mal maps to be applied on dif­fer­ent scale geom­e­try. My solu­tion was and still is to ignore the deter­mi­nant and just nor­mal­ize \mathbf{T} and \mathbf{B} to whichever of them is largest, as seen in the code. This solu­tion pre­serves the rel­a­tive lengths of \mathbf{T} and \mathbf{B}, so that a skewed or stretched cotan­gent space is sill han­dled cor­rectly, while hav­ing an over­all scale invariance.

Non-​perspective optimization

As the ulti­mate opti­miza­tion, I also con­sid­ered what hap­pens when we can assume \Delta \mathbf{p_1} = \Delta \mathbf{p_2}_\perp and \Delta \mathbf{p_2} = \Delta \mathbf{p_1}_\perp. This means we have a right tri­an­gle and the per­pen­dic­u­lars fall on the tri­an­gle edges. In the pixel shader, this con­di­tion is true when­ever the screen-​projection of the sur­face is with­out per­spec­tive dis­tor­tion. There is a nice fig­ure demon­strat­ing this fact in [4]. This opti­miza­tion saves another two cross prod­ucts, but in my opin­ion, the qual­ity suf­fers heav­ily should there actu­ally be a per­spec­tive distortion.

Putting it together

To make the post com­plete, I’ll show how the cotan­gent frame is actu­ally used to per­turb the inter­po­lated ver­tex nor­mal. The func­tion perturb_normal does just that, using the back­wards view vec­tor for the ver­tex posi­tion (this is ok because only dif­fer­ences mat­ter, and the eye posi­tion goes away in the dif­fer­ence 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 tex­ture coor­di­nate ori­gin at the start of the image pixel data. The tex­ture coor­di­nate (0,0) is in the cor­ner of the pixel where the image data pointer points to. Con­trast this to most 3-​D mod­el­ing pack­ages that place the tex­ture coor­di­nate ori­gin at the lower left cor­ner in the uv-​unwrap view. Unless the image for­mat is bottom-​up, this means the tex­ture coor­di­nate ori­gin is in the cor­ner of the first pixel of the last image row. Quite a dif­fer­ence!
An image search on Google reveals that there is no dom­i­nant con­ven­tion for the green chan­nel in nor­mal maps. Some have green point­ing up and some have green point­ing down. My artists pre­fer green point­ing up for two rea­sons: It’s the for­mat that 3ds Max expects for ren­der­ing, and it sup­pos­edly looks more nat­ural with the ‘green illu­mi­na­tion from above’, so this helps with eye­balling nor­mal maps.

Sign Expan­sion

The sign expan­sion deserves a lit­tle elab­o­ra­tion because I try to use signed tex­ture for­mats when­ever pos­si­ble. With the unsigned for­mat, the value 0.5 can­not be rep­re­sented exactly (it’s between 127 and 128). The signed for­mat does not have this prob­lem, but in exchange, has an ambigu­ous encod­ing for -1 (can be either -127 or -128). If the hard­ware is inca­pable of signed tex­ture for­mats, I want to be able to pass it as an unsigned for­mat and emu­late the exact sign expan­sion in the shader. This is the ori­gin of the seem­ingly odd val­ues in the sign expansion.

In Hind­sight

The orig­i­nal arti­cle in ShaderX5 was writ­ten as a proof-​of-​concept. Although the algo­rithm was tested and worked, it was a lit­tle expen­sive for that time. Fast for­ward to today and the pic­ture has changed. I am now employ­ing this algo­rithm in real-​life projects for great ben­e­fit. I no longer bother with tan­gents as ver­tex attrib­utes and all the asso­ci­ated com­plex­ity. For exam­ple, I don’t care whether the COLLADA exporter of Max or Maya (yes I’m rely­ing on COLLADA these days) out­put usable tan­gents 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 miss­ing, because It’s all nat­ural: There is a geom­e­try, there are tex­ture coor­di­nates and there is a nor­mal map, and just works.

Take Away

There are no ‘tan­gent frames’ when it comes to nor­mal map­ping. A tan­gent frame which includes the nor­mal is log­i­cally ill-​formed. All there is are cotan­gent frames in dis­guise when the frame is orthog­o­nal. When the frame is not orthog­o­nal, then tan­gent frames will stop work­ing. Use cotan­gent frames instead.


[1] James Blinn, “Sim­u­la­tion of wrin­kled sur­faces”, SIGGRAPH 1978
http://research.microsoft.com/pubs/73939/p286-blinn.pdf

[2] Mark Peercy, John Airey, Brian Cabral, “Effi­cient Bump Map­ping Hard­ware”, SIGGRAPH 1997
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.42.4736

[3] Mark J Kil­gard, “A Prac­ti­cal and Robust Bump-​mapping Tech­nique for Today’s GPUs”, GDC 2000
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.18.537

[4] Chris­t­ian Schüler, “Nor­mal Map­ping with­out Pre­com­puted Tan­gents”, ShaderX 5, Chap­ter 2.6, pp. 131 – 140

[5] Colin Barré-​Brisebois and Stephen Hill, “Blend­ing in Detail”,
http://blog.selfshadow.com/publications/blending-in-detail/

33 thoughts on “Followup: Normal Mapping Without Precomputed Tangents

  1. Out of inter­est, what impli­ca­tions does this have for tangent-​space cal­cu­la­tions when you *bake* normal-​maps (for exam­ple, inside 3dsmax).
    Wouldn’t you have to also use this method when cre­at­ing the orig­i­nal normal-​map?

    • Hi MoP, absolutely, you’re cor­rect in your assumption.

      If your nor­mal map is the result of sam­pling high poly geom­e­try, then the nor­mal map sam­pler should use the same assump­tions about tan­gent space than the in the engine, if you want high­est fidelity. This is gen­er­ally true, whether you use pre­com­puted tan­gents or not.

      If on the other hand you have a painted nor­mal map, let’s say, a generic rip­ple tex­ture, this just fol­lows what­ever tex­ture map­ping is applied to the mesh. If this map­ping is sheared or stretched, and so vio­lates the square patch assump­tion, the algo­rithm from above is still able to light it correctly.

    • Thanks for the fast reply, Chris­t­ian!
      Absolutely, that makes sense… so I’m won­der­ing if, for “per­fect” results, if peo­ple are bak­ing their nor­mal maps in 3dsmax or Maya or XNor­mal or what­ever, those pro­grams will need to have their tan­gent space cal­cu­la­tions updated/​reworked to match the in-​game cal­cu­la­tion?
      I’m guess­ing that cur­rently the 3dsmax cal­cu­la­tion is not exactly synced with the method you describe here… or am I wrong?

    • You would be sur­prised to know that the method in 3dsMax or any­where else is most likely not synced with any engine, even when using the clas­sic method. There is an entire the­sis devoted to this topic (look for Mikkelsen here). If you want such per­fec­tion, then the engine should have its own tools for bak­ing nor­mal 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 nor­mal maps were per­fect … they imported LWOs into the engine and baked the normal-​maps using the game engine so that the cal­cu­la­tions would match up per­fectly.
      I haven’t seen any other engine (released pub­li­cally) do that yet, but I work in game devel­op­ment and I know that we have been try­ing to solve this prob­lem for years with no sat­is­fac­tory results. Cur­rently the best method seems to be to mod­ify a third-​party pro­gram (eg. XNor­mal) to cal­cu­late tan­gent space in exactly the same way as the tar­get engine cal­cu­lates it, as this is the only way to ensure the normal-​maps will be absolutely correct.

  2. Pingback: Normal Mapping Without Precomputed Tangents « Interplay of Light

  3. I am con­fused to what space your TBN matrix the nor­mal from the nor­mal map tex­ture con­verts! You need this in order to do light­ing cal­cu­la­tions. Should I trans­form the light vec­tor and view vec­tor with the TBN matrix, or leave them in cam­era space?

    Do you assume in the frag­ment shader that g_​vertexnormal, g_​viewvector are in the ver­tex shader mul­ti­plied with the gl_​NormalMatrix and gl_​ModelViewMatrix respec­tively? Oth­er­wise how do you account for the trans­for­ma­tions on the object.

    Thanks

    • Sorry I meant in the pre­vi­ous post “light vec­tor” and “eye vec­tor” instead of view vec­tor for the fist paragraph.

    • Hi cinepi­vates,
      the TBN matrix that I build in the pixel shader is the trans­form from cotan­gent space to world space. After the vec­tor from the nor­mal map is mul­ti­plied with the TBN matrix, it is a world space nor­mal vec­tor. This is due to the fact that both the inter­po­lated ver­tex nor­mal and the view vec­tor are sup­plied in world space by the ver­tex shader (not shown). Alter­na­tively, if the ver­tex shader sup­plies these vec­tors in eye space, the pixel shader should con­struct the TBN matrix to con­vert into eye space instead. So you can choose your way.

    • Hi
      thanks for the reply.
      Since you are mul­ti­ply­ing per pixel with a mat3 (TBNxNor­mal) on the pixel shader have you mea­sured any per­for­mance decrease com­pared to the more tra­di­tional method of pre­com­puted Tan­gents where you usu­ally only trans­form in the ver­tex shader the light,eye vec­tors into tan­gent space using the TBN matrix.

    • I don’t use the TBN in the ver­tex shader. That is a thing of the past when there was pixel shader model 1.x. It pre­vents you to use world space con­stants in the pixel shader. For exam­ple, a reflec­tion vec­tor to look up a world space envi­ron­ment map. So the matrix mul­ti­ply by TBN in the pixel shader has always been there for me.

  4. Hi,I just won­der­ing if light­ing cal­cu­la­tions can be done in tan­gent space.My tex­tures could not match the Square Patch Assumption,but I don’t want to lose tan­gent space lighting,since some tech­niques are par­tic­u­lar designed for that.

    • Hi April,
      this is tricky, as light­ing in tan­gent space does only work if it is orthog­o­nal. Think about it: the angle between any vec­tors (say, the angle between \mathbf{N} and \mathbf{L}) will change under a non-​orthogonal trans­for­ma­tion. So dot(N,L) is going to result in a dif­fer­ent value, depend­ing 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 opti­miza­tion men­tioned in the post), and use that to trans­form \mathbf{L}, \mathbf{V}, \mathbf{H} and any other vec­tor you need into tan­gent space, and do the light­ing com­pu­ta­tions there.

    • Thanks for reply :)
      I man­aged to orthog­o­nal­ize the matrix and then used the inverse-​transpose one to do light­ing in world space, the result became a mess.I checked the orgin matrix, and found out the matrix can not be inversed where UV mir­rors. The deter­mi­nant() func­tion returns 0.
      Still look­ing for reason.

    • I’m not sure I can fol­low your argu­ment. If you want to do light­ing in world space, then you don’t need to change any­thing. If you want to do light­ing in tan­gent space instead (which only yields sim­i­lar results if the tan­gent space is roughly orthog­o­nal), you’d need a matrix to con­vert your light, view, etc vec­tors into tan­gent space. Edit: For this you need the inverse of the TBN. You can get the inverse-​tranpose already very sim­ply by ignor­ing the two cross prod­ucts and using dp2perp = dp1 and dp1perp = dp2. Then the trans­pose of this would be the inverse of the TBN (ignor­ing scale). In the shader you don’t need to explic­itly trans­pose, you can just mul­ti­ply with the vec­tors from the left (eg vector * matrix instead of transpose(matrix) * vector). Then you can orthog­o­nal­ize this matrix if you want, but this won’t help much if the tan­gent space is not orthog­o­nal to begin with. I don’t under­stand why you are will­ing to go though such hoops instead of sim­ply doing the light­ing in world space.

  5. Fan­tas­tic arti­cle! I switched my engine from using pre­cal­cu­lated tan­gents to the method your describe - very easy to imple­ment. I’m using WebGL, but I’m pre­sum­ing my find­ings will cor­re­spond to what OpenGL + GLES pro­gram­mers see. On desk­top, I don’t really notice much of a per­for­mance dif­fer­ence (if any­thing, 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 pre­com­puted tan­gents. Seems dFdx and dFdy are par­tic­u­larly slow on mobile GPUs. Just cut­ting out those calls takes my FPS from ~24FPS to ~30FPS. So although I think this method is ele­gant to the extreme, I’m not sure it’s fast enough to be the bet­ter option (at least, not on mobile). I really hope some­one can con­vince me oth­er­wise though! :)

    • Hi Will, thanks for shar­ing. Of course those addi­tional ~14 shader instruc­tions for cotangent_frame() are not free, espe­cially not on mobile (which is like 2005 desk­top, the time when the arti­cle was orig­i­nally writ­ten). For me today, this cost is invis­i­ble com­pared to all the other things that are going on, like mul­ti­ple lights and shad­ows and so forth. On the newest archi­tec­tures like the NVidia Fermi, it could already be a per­for­mance win to go with­out pre­com­puted tan­gents, due to the rea­sons men­tioned in the intro­duc­tion. But while that is nice, the main rea­son I use the method is the boost in productivity.

  6. Pingback: Martin Codes – Cool Link Stash, January 2013

  7. For me the equa­tions in this are screwed up. The are ran­dom LaTeX things in the images (Delta­mathbf). Same with Safari, Chrome and Firefox.

    Would be nice if you could fix that.

  8. Hallo there, I have fixed the math for­mu­lae in the post. It turned out to be an incom­pat­i­bil­ity between two plu­g­ins, and that got all \LaTeX back­slashes eaten. Sorry for that! Should there be a bro­ken for­mula that I have over­looked, just drop a line.

  9. So glad I found your blog. Though I am an artist these more tech­ni­cal insights really help me in under­stand­ing what to com­mu­ni­cate to our coders to estab­lish cer­tain looks.

  10. Hi, I want to ask you to elab­o­rate on 2 fol­low­ing 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 math­e­mat­i­cal deriva­tion of this fact. Also it is unclear for me why deltaU = dot(T, deltaP), and how this fol­lows from def­i­n­i­tion of gra­di­ent. Oth­er­wise I found this arti­cle very excit­ing and my exper­i­ments showed that imple­men­ta­tion works very well and is a good drop-​in replace­ment for con­ven­tional tan­gent basis.

    • Hi Mykhailo,
      thanks for shar­ing your expe­ri­ence. Indeed, I did not explain why \mathbf{N} \times \mathbf{P}_v / |\mathbf{N}|^2 = \nabla u, and that may not be obvi­ous, so here it goes: The gra­di­ent vec­tor is always per­pen­dic­u­lar to the iso-​surface (aka. “level-​set”). In a skewed 2-​D coor­di­nate sys­tem, the iso-​line of one coor­di­nate is sim­ply the other coor­di­nate axis! So the gra­di­ent vec­tor for the u tex­ture coor­di­nate must be per­pen­dic­u­lar to the v axis, and vice versa. (The scale fac­tor makes it such that the over­all length equals the rate of change, which is depen­dent on the assump­tion that |\mathbf{N}| = |\mathbf{P}_u \times \mathbf{P}_v|.) Your sec­ond ques­tion is also related to the fact that the gra­di­ent vec­tor is per­pen­dic­u­lar to the iso-​surface. If a posi­tion delta is made par­al­lel to the iso-​surface, then the tex­ture coor­di­nate doesn’t change, because in this case the dot prod­uct is zero.

    • Thank you for an answer, now every­thing is a bit clearer. Still was able to fully fig­ure it out only after I under­stood that para­metri­sa­tion of u, v is lin­ear in the plane of every tri­an­gle. For some rea­son I just missed that fact. And after you men­tioned that gra­di­ent is per­pen­dic­u­lar to iso-​line every­thing made sense. And now it make sense why change of u is pro­jec­tion of posi­tion delta onto gra­di­ent vector.

  11. Hi Chris­t­ian, thanks for sharing!

    I am work­ing on an enhanced ver­sion of the Crytek-​Sponza scene and I had prob­lems with my per-​pixel nor­malmap­ping shader based on pre­com­puted tan­gents. I had most sur­faces lighted cor­rectly, but in some cases they weren’t. It turned out that - unknown to me - some faces had flipped UVs (which is not that uncom­mon) and there­fore the cotan­gent frame was messed up, because the inverse of the tan­gent was used to cal­cu­late the binor­mal as the crossprod­uct with the ver­tex normal.

    Now, I replaced it with your approach and every­thing is alright; it works instantly and all errors van­ished. Awe­some!! :-)

  12. Hi,

    Does the com­pu­ta­tion still work if the mesh nor­mal is in view space, and the g_​viewvector = vec3(0, 0, -1) ?

    Cheers :)

    • Yes, but you must pro­vide the ver­tex posi­tion in view space also. The view vec­tor is used as a proxy to dif­fer­en­ti­ate the ver­tex posi­tion, there­fore a con­stant view vec­tor will not do.

    • Ah ok, I just nor­mal­ized the view posi­tion and its work­ing perfectly

      Thanks!

  13. Hi,
    Your idea seem to be very inter­est­ing.
    But I’ve tried it in real scene, and found a glitch - if uv (tex­ture coor­di­nates) are mir­rored, nor­mal is also become mirrored.

    Is there any way to fix it?

    • The nor­mal itself should not be flipped, only the tan­gents. If the u tex­ture coor­di­nate is mir­rored, then the \mathbf{T} tan­gent should reverse sign and sim­i­lar with v and the \mathbf{B} tangent.

Leave a Reply

Your email address will not be published.


7 + = nine

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>