# Bump mapping WITH precomputed tangents?

Most bump mapping (meaning black and white height map) uses Tangents/Bitangents computed from Position using dFdx/y. And they also compute the offsets/derivatives of the height map using dFdx/y. The standard seems to be to not use precomputed Tangents/Bitangents. If you search for bump mapping, everything to do with it seems to be like this. I am trying to get a setup working that does use precomputed tangents, as these are higher quality. But I do not know how to adapt the math to work with them.

Here is an example of typical bump mapping based on Mikkelsen2010. (This is actually the setup that Blender uses for the bump mapping node in its Eevee render engine. I have recreated it in the Malt render engine.)

void bump_without_precomputed_tangents(
float strength, float dist, float height, float invert, out vec3 result)
{
vec3 N = normalize(NORMAL);
dist *= gl_FrontFacing ? invert : -invert;

vec3 dPdx = dFdx(POSITION);
vec3 dPdy = dFdy(POSITION);

//Get surface tangents from normal.
vec3 Rx = cross(dPdy, N);
vec3 Ry = cross(N, dPdx);

//Calculate height derivatives (usually done in another function)
vec2 dHd = vec2(dFdx(height), dFdy(height));

float det = dot(dPdx, Rx);

vec3 surfgrad = dHd.x * Rx + dHd.y * Ry;

strength = max(strength, 0.0);

result = normalize(abs(det) * N - dist * sign(det) * surfgrad);
result = normalize(mix(N, result, strength));

#else
result = (NORMAL);
#endif
}


Here's a test setup showing a gradient sphere height map texture:

And the Normal from using it with the above code (using Malt 1.0 in Blender 3.1):

You can see there are problems of ridge-like noise and the geometry showing in the Normal. These are normal for this style of bump mapping. The ridges are caused by using dFdx/y for the height map derivatives. The mesh showing is partially from that, and partially from using dFdx/y on Position to get the Tangent/Bitangent. If proper precomputed Tangents are used, these problems will be reduced. (The derivatives can also be improved in various ways.)

The problem is that the precomputed tangent/bitangent are Normalized, but in the above code, dPdx/y, Rx/y, and dHd are all very low values. If I simply replace Rx/y with the precomputed Tangent/Bitangent, it doesn't work due to this. I am trying to figure out how to get dHd Normalized without causing problems. Or, the precomputed Tangents need to be scaled into the proper range without reintroducing these problems. (The precomputed tangents are provided by the Malt render engine.)

Here is code where I am bringing in the precomputed tangents and Normalizing dHd. But this does not give the proper Normals in the end.

void bump_normalized(
float strength, float dist, float height, float invert, out vec3 result)
{
vec3 T = get_tangent(0);
vec3 B = get_bitangent(0);
vec3 N = normalize(NORMAL);

dist *= gl_FrontFacing ? invert : -invert;

vec3 dPdx = dFdx(POSITION);
vec3 dPdy = dFdy(POSITION);

//Calculate height derivatives.
vec2 dHd = vec2(dFdx(height), dFdy(height));

//Normalize derivs so they work with Normalized Tangent. Avoid div by 0.
dHd = length(dHd) == vec2(0.0) ? vec2(0.0) : normalize(dHd);

float det = dot(normalize(dPdx), T);

vec3 surfgrad = (dHd.x * T + dHd.y * B);

strength = max(strength, 0.0);

//result = normalize(abs(det) * N - dist * sign(det) * surfgrad);
result = normalize(N - dist * surfgrad);
result = normalize(mix(N, result, strength));

#else
result = (NORMAL);
#endif
}


The Normal looks like this. It is incorrect.

In this code, I also do not know what to do with the det, as it is based on dPdx, which isn't needed now (its usual result line is commented out for now.)

I have other setups that successfully use the precomputed tangents and UV offset based derivatives instead, and these work fine (and are clean as no dFdx/y is involved.)

Can anyone tell me what I'm doing wrong here, or if there's some other way entirely to approach this? I want to be able to properly use Normalized T and B.

• "Most bump mapping (meaning black and white height map) uses Tangents/Bitangents computed from Position using dFdx/y." While some tutorials may do that, that's not how it's generally done in any remotely professional code. May 23, 2022 at 13:56
• @NicolBolas I am aware that this is an older way of doing things. It doesn't surprise me to hear that its not being done this way in modern professional setups. So are they using precomputed tangents then? Can you point me to any examples that may answer the question? May 23, 2022 at 19:46

Usually the tangent and normal for a given vertex TBN is passed in as a vertex attribute, something along the lines of this...

x,y,z //vertex position
x,y,z //normal
x,y,z,w //tangent where "w" represents the direction of the bitangent
u,v //texture coordinates


Given that set of attributes: The bitangent is computed by taking the cross product of vertex attributes tangent and normal, then multiplied by the w coordinated of the tangent to get the correct direction. The w component is either 1 or -1. This gives us our TBN.

The fragment shader uses the UV coordinates to lookup the associated normal from a normal map. Then uses the TBN computed above to compute a normal that will be used for lighting.

That's pretty much it.

All the hard work is precomputed. A normal map is computed from the height map. The TBN for the mesh vertex attributes is also precomputed.

One way to compute the TBN is using the UV coordinates and vertex positions to create a system of equations which can be solved to find the tangent and bitangent.

The TBN values are also typically "averaged" together with neighboring TBN vectors, and finally orthonormalized to get a better tangent frame.

When computing the Tangent frame, u and v coordinates for the vertex attributes give distances along the t and b axes and are aligned to the x and y directions of the texture map.

This means that the T and B vectors of the TBN rotate vectors from the normal map in the x, y plane to align with triangles in the mesh.

If we then take that tangent frame and attempt to use it with vectors derived from derivatives of values in a height map, we may end up with T and B vectors that are in the correct plane, but those vectors will be rotated so they point in directions aligned with texture space of the normal map they are created for. This will result in transformed normals that are correct when they align with the original texture map, but will otherwise point in directions that give unusable results.

Another approach to this problem that is somewhat similar to what you are describing is called "derivative maps". I recommend looking up that technique.

• Yes, I have a TBN like this. In my code example, it is coming from vertex attributes. I want to use it with a height map though, not with a pre-made Normal map. The goal is bump mapping with arbitrary height input, but using precomputed Tangents instead of position computed tangents, since those are lower quality. May 26, 2022 at 18:40
• Actually Mickelson continued work on the paper you are referencing and has a newer published paper that goes into good detail on the subject including the math in the journal of computer graphics including a working program. You can find it here: jcgt.org/published/0009/03/04 May 30, 2022 at 0:30
• Yes, I am familiar with it. It does not address this specific issue as far as I can tell. I may be missing something though. It has the bump map from unparametrized surfaces (same as above), and it has the triple tap forward differencing for image textures (which I have working.) But does not seem to have arbitrary height input with precomputed tangents. May 30, 2022 at 1:07
• I am also aware of derivative maps, but they don't solve this issue either. The question is about Normalization, and issues with this specific combination of Precomputed Tangent and dFdx/y derivatives. I appreciate the other info, but the question is not about alternatives. I am already aware of them. May 31, 2022 at 23:33
• Right, but the precomputed tangents that are computed with UV coords in texture space are not in the same space as derivate's that are computed from screen space using the chain rule and the only way to solve the problem is either translate the TBN or compute directly from the height map both of which make the entire endeavor pointless, since in the first place the alu cost is way to much and in the second place is how the normals are computed to begin with. May 31, 2022 at 23:51