Tangent space - Lengyel vs ShaderX5 - difference ?

Started by
7 comments, last by Martin Perry 8 years, 6 months ago

I have been studiing tangent space calculations and came across two solutions

E. Lengyel - http://www.terathon.com/code/tangent.html

ShaderX5 solution - (solution is also desribed here http://www.thetenthplanet.de/archives/1180)

What is the difference between tangent computed one or another way? Lengyel solution seems "easier" but I am no quite sure of its orthogonality to the normal vector of the tangent plane.

Advertisement

Um? They're completely different, unrelated techniques.

One calculates a bi[normal|tangent] base (or rather, two of three components, since the third is the cross of these two) from vertex data on the CPU and stores normal and tangent in a vertex stream. Which is what everybody did 15 years ago, except that hardly anyone actually did the calculation himself since your favorite 3d modelling software will (normally) readily generate tangents for you. Some free lightmap baker software (Something FX? I forgot the name!) would do them per-pixel, too, if you didn't have professional baking software. That was more correct than interpolating per-vertex data and looked somewhat nicer, but you needed an additional texture.

The other technique calculates, or rather estimates, the tangent (or rather, cotan­gent in the follow-up) frame in the fragment shader. Which used to be a tidbit expensive 10 years ago, with partial derivatives and such, but I don't think it's noticeable at all now. Note that ALU power has gone way more than TEX during these years, ALU no longer being a bottleneck virtually anywhere.

Unrelated? They both calculate tangent (and bitangent, or at least I understand it that way). The first store data in vertex, yes, but the second can to. So the first can run on GPU, because the difference they used are the same in both techniques.

Well, I'm saying "different, unrelated" because they take completely different approaches. The first precisely calculates two vectors from a triangle, consuming three vertices of the mesh at a time in a preprocessing step (no, this couldn't be done on the GPU), and uses two vertex streams for the two vectors. It then has the hardware interpolate them (which is mathematically incorrect), renormalizes and does a cross product. Which looks OK, despite being incorrect.

The other has no preprocessing step (OK, I lied... you still need to have a normal, of course -- but this is a step that you do not see). Instead, it makes a per-pixel estimate based on partial derivatives (the dFdx and dFdy instructions in the code snippet). It does not need another vertex stream for the binormal (or any other data, in particular it does not need all three vertices of the triangle), and it achieves a quality that is just as good (acutally, better).

Unrelated? They both calculate tangent (and bitangent, or at least I understand it that way). The first store data in vertex, yes, but the second can to. So the first can run on GPU, because the difference they used are the same in both techniques.

I recently read the Shader X5 article and I tend to agree. It was not obvious to me what difference it makes to calculate the tangent space frame in the fragment shader rather than calculating it per vertex. I would certainly appreciate further discussion of the two techniques.

The first precisely calculates two vectors from a triangle, consuming three vertices of the mesh at a time in a preprocessing step (no, this couldn't be done on the GPU), and uses two vertex streams for the two vectors.

I suppose this assumes the absence of geometry shaders?

It then has the hardware interpolate them (which is mathematically incorrect), renormalizes and does a cross product. Which looks OK, despite being incorrect.

Could you elaborate on why this is less correct than using the derivatives in the fragment stage?

Well, I'm saying "different, unrelated" because they take completely different approaches. The first precisely calculates two vectors from a triangle, consuming three vertices of the mesh at a time in a preprocessing step (no, this couldn't be done on the GPU), and uses two vertex streams for the two vectors. It then has the hardware interpolate them (which is mathematically incorrect), renormalizes and does a cross product. Which looks OK, despite being incorrect.

The other has no preprocessing step (OK, I lied... you still need to have a normal, of course -- but this is a step that you do not see). Instead, it makes a per-pixel estimate based on partial derivatives (the dFdx and dFdy instructions in the code snippet). It does not need another vertex stream for the binormal (or any other data, in particular it does not need all three vertices of the triangle), and it achieves a quality that is just as good (acutally, better).

But If I look at the first approach and compare the values needed to compute tangent with ShaderX5 book, they calculate du1 = u1 - u0 (and other differences similar way), which is the same as the first approach does.

I understand why it is good to move calculations to the GPU to pixel shader (you can calculate it independently on geometry, plus today you can generate or tesselate geometry). What I dont understand from both articles is, why I cant use the first one in shader, because for a single triangle, if i wrote solution on the paper, they both use the same differences.


I recently read the Shader X5 article and I tend to agree. It was not obvious to me what difference it makes to calculate the tangent space frame in the fragment shader rather than calculating it per vertex. I would certainly appreciate further discussion of the two techniques.

We need to back up a bit.

The idea of normal mapping is:

1) Artist creates super-high-poly mesh.

2) Artist creates low-poly mesh.

3) The normals from #1 are "baked" into a texture, using the UV's of mesh #2.

4) When drawing mesh #2, instead of using it's actual per-vertex normals, use it's texture coordinates to sample the normal-map.

This allows mesh #2 to appear to have the same detail that mesh #1 originally had (except around the silhouette).

However, this has some issues. If you animate/transform mesh #2, the normal map/texture will be incorrect -- it's normals will no longer match the animated/transformed mesh.

A nice, flexible, robust solution is to add some more steps -- "tangent-space normal mapping" (which is so common that most people just call it "normal mapping"):

3.B) Transform the normals in the normal-map from model-space to tangent-space. This requires that every vertex defines what tangent-space is, so every vertex requires a tangent, bitangent and a normal.

4.B) After sampling the normal map, transform the sampled value from tangent-space back to model/world/view/etc space. This requires that the exact same normal/tangent/bitangent values from step 3.B are used.

Now the mesh can be animated/transformed without any issues.

So when it comes to different methods for "inventing" a tangent-basis per vertex, it doesn't matter as long as the exact same values are used when authoring the normal map, and when rendering at runtime.

You can think of tangent-space normal maps as a kind of compression -- if you use the same basis during encoding and decoding, then it can be (nearly) lossless. If you use different basis for each step, then you'll rotate, flip, scale, and/or skew your normals...

If your art is coming from a regular art tool, then it will create the normals/tangents/bitangents for you -- just make sure to preserve the exact values that have been generated by it. Otherwise your artists will forever complain about normal maps being "not quite right". Your artists also need to be educated about how tangent-space "compression" works, so that they can successfully retain the same tangent-space data all the way through their workflow into your game.

If you're art is being made by your own tools/procedures, I hear that Morten S. Mikkelsen's "mikktspace" algorithm is somewhat of a standard these days.

https://svn.blender.org/svnroot/bf-blender/trunk/blender/intern/mikktspace/mikktspace.h

https://svn.blender.org/svnroot/bf-blender/trunk/blender/intern/mikktspace/mikktspace.c

The idea of normal mapping is:

That's not the whole idea, actually. smile.png

TSNM is also useful with level editing tools that generate level geometry (CSG/patches/terrains) since it's not possible to know anything about surfaces in advance (which is necessary for making object-space normal maps).

So when it comes to different methods for "inventing" a tangent-basis per vertex, it doesn't matter as long as the exact same values are used when authoring the normal map, and when rendering at runtime.

It seems to be really hard to get this wrong using any of the currently developed tangent space generation methods. Tangents can be flipped and swapped (as these transformations are easy to undo) but apart from that I think that extra position/texcoord transformation effort would have to be applied to get this wrong. For example, I've been using this tangent space generation algorithm with normal maps baked in Blender. Only issue was, Blender flips the Y axis (I like when Y+ = G+ = "down" in texture space, makes things easy to read)

If you animate/transform mesh #2

While we're poking around TSNM, does anyone know if, for skinned meshes, generating tangent space in the pixel shader is faster than skinning and interpolating it from vertices?

Lengyel solution seems "easier" but I am no quite sure of its orthogonality to the normal vector of the tangent plane.

Tangent vector is specifically orthogonalized and normalized there. Since the other tangent is generated with "cross(N,T.xyz)*T.w", everything seems fine to me.

What's been bothering myself more about Lengyel's approach is that he weighs tangents by the equivalent of inverse triangle area (area=cross(p2-p1,p3-p1)/2). Wouldn't it make more sense (and avoid a FP division) to scale by (the equivalent of) triangle area?

What's been bothering myself more about Lengyel's approach is that he weighs tangents by the equivalent of inverse triangle area (area=cross(p2-p1,p3-p1)/2). Wouldn't it make more sense (and avoid a FP division) to scale by (the equivalent of) triangle area?

Are you talking about the value of r, which is 1.0F / (s1 * t2 - s2 * t1)? This divides out the "area" of the triangle in texture space so that the scale of the texture map doesn't matter, but does not have anything to do with the geometric position of the vertices. The tangents are still weighted based on the geometric area of the triangles because the variables x1, y1, z1, x2, y2, and z2 are not normalized in any way.

Are you talking about the value of r, which is 1.0F / (s1 * t2 - s2 * t1)?

Yes, that's exactly what I was talking about. Thanks for the explanation!

This topic is closed to new replies.

Advertisement