Search Unity

Compute view-space tangent and bitangent from view-space normal

Discussion in 'Shaders' started by metaleap, Jul 1, 2014.

  1. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    I confess the underlying tangent maths is beyond me but nonetheless for some anisotropic-specular experiments I seem to need view-space tangent and bitangent. I'm in a g-buffer that has view-space normals but setting up 2 additional g-buffers for those vectors (I do indeed have them in my g-buffer-filling "not yet lit" vert+frag shaders) is not feasible. Now I came across this snippet:

    Code (csharp):
    1.  
    2. void computeTangentVectors(float3 norm, out float3 tang, out float3 bitang) {
    3. tang = abs(norm.x) < 0.999 ? float3(1,0,0) : float3(0,1,0);
    4. tang = normalize(cross(norm, tang));
    5. bitang = normalize(cross(norm, tang));
    6. }
    7.  
    Some of you know already almost-intuitively what tangent-space is and how it works: is the above "correct", will it likely give me what I need? Without the solid fundamentals deeply ingrained in the brain (yes, yes, it's on my to-do list :D ) this is hard to verify visually especially when experimentally implementing new algorithms where I might well introduce other errors...

    (And if the above is correct and sufficient... why the hell am I sending out tang+bitang interpolators from all my vertex shaders in addition to view-space normals.... can't be cheaper than 2 normalize+cross calls in frag surely with all the added interpolation and memory consumed?)
     
  2. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    The normal is the vector perpendicular to the surface. This is well known.

    The tangent is the vector orthogonal to the normal and aligned with the u-direction of a mapping channel.

    The bitangent is the vector that completes this set of orthogonal basis vectors.

    So the calculation of the bitangent is correct in the code above, but the calculation of the tangent does not take the mapping channel into account. So, the code will give you an orthogonal set of vectors that define a tangent space. I'm not sure whether this basis suits the requirements for the anisotropic specular you have in mind.
     
  3. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    Thanks @jvo3dc for the basic explanation! Yeah I just experimentally replaced tang/bitang v2f interpolants with the above computation and it does screws up the final resulting bumpmap-influenced surface normals as used for lighting but only ever-so-slightly.. that confirms what you're saying as I applied this pre-bumpmapping. Will have to try and see what it does in screen-space :D
     
    Last edited: Jul 1, 2014
  4. Nims

    Nims

    Joined:
    Nov 11, 2013
    Posts:
    86
    I am new to the whole subject...I might have misunderstood your question, but if all you are trying is to get the tangent, binormal/bitangent and normal in View Space then this all you really need:

    Code (Cg):
    1. float4x4 modelMatrix = _Object2World;
    2. float4x4 modelMatrixInverse = _World2Object;
    3.  
    4. float3 tangentWS = normalize (float3(mul(modelMatrix, float4(float3(input.tangent), 0.0))));
    5. float3 normalWS = normalize (float3(mul(float4(input.normal, 0.0), modelMatrixInverse)));
    6. float3 binormalWS = normalize (cross(normalWS, tangentWS) * input.tangent.w);
    input is just the input struct with semantics NORMAL and TANGENT.
    Of course you can substitute modelMatrix and modelMatrixInverse in the last 3 line to _Object2World and _World2Object respectively to make this only 3 lines.
     
  5. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    Thanks, but while my normals and positions are indeed in view-space my shader is in screen-space so consequently any code relying on "object space" won't work meaningfully :D

    The good news is, my above snippet does seem to yield perceptually acceptable results so I guess it's OK.. ;)

    Meanwhile noticed that the Ward implementation at http://content.gpwiki.org/D3DBook:(Lighting)_Ward uses almost-identical code to get these vectors in "we have no object-space right now" view-space so that's reassuring too
     
  6. Nims

    Nims

    Joined:
    Nov 11, 2013
    Posts:
    86
    Just looking at the code in your first post...I am going to say that I am not sure that the resulting normal and tangent are orthogonal to each other.
    While the pair normal and bitangent, and the pair tangent and bitangent are orthogonal....
     
  7. jvo3dc

    jvo3dc

    Joined:
    Oct 11, 2013
    Posts:
    1,520
    Code (csharp):
    1. tang = normalize(cross(norm, tang));
    This makes the normal and tangent orthogonal to each other. The problem is that the tangent is not aligned with the texture (normal map) on the surface. There is also no way to do this without storing some extra information about the tangent direction in a full screen buffer I think.
     
  8. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    What is it you're trying to do? There might be a better way...
     
  9. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    Well since I'm in screen-space sampling into a pre-rendered "view-space normals" g-buffer (custom, not Unity's out-of-box depth+normals rendertex) .. as far as we care, there could be "no normal-mapping has ever occurred"... the concept of a normal map texture does not exist at this point. Still, we have a view-space normal (ie. norm.X increases further the more "to the right screen-edge" a normal points etc.) there exist 2 orthogonal (let's call them tang+bitang) vectors that need to be found.

    Implemented anisotropic specular https://github.com/wdas/brdf/blob/master/src/brdfs/disney.brdf (but see also http://content.gpwiki.org/D3DBook:(Lighting)_Ward for a more old-school approach) in my custom-coded own-deferred-pipeline... visually it "seems by and large to look as was probably expected well I'm not totally certain" but then I'm still a little worried that apparently while bitang is correct the tang calculation seems to be arbitrary/non-accurate. In D3DBook:Ward they use the "arbitrary vector epsilon (1,0,0)" for the tangent. Disney do basically the same for all normals where abs(norm.x) < 0.999 so that means probably well over 90% of normals realistically, for the others they use an eps of (0,1,0).

    Would love to grasp the logic behind this arbitrary epsilon vec :D
     
  10. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    Hmm, I'm not really certain... could be they're trying to stop some kind of singularity; when norm.x = 1 or -1, you would wind up with the tangent being a zero vector.

    Code (csharp):
    1.  
    2. // Generally you'd want to orthonormalize the tangent to be perpendicular to the normal.
    3. // Using Gram-Schmidt, that would be...
    4. tang = float3 (1.0, 0.0, 0.0);
    5. tang = normalize (tang - dot (tang, norm) * norm);
    6.  
    7. // That breaks down into the same result, no matter what norm.x is...
    8. tang = normalize (float3 (1.0, 0.0, 0.0) - dot (float3 (1.0, 0.0, 0.0), norm) * norm);
    9. tang = normalize (float3 (1.0, 0.0, 0.0) - float3 (norm.x, 0.0, 0.0) * norm);
    10. tang = normalize (float3 (1.0, 0.0, 0.0) - float3 (norm.x * norm.x, 0.0, 0.0));
    11. tang = normalize (float3 (1.0 - (norm.x * norm.x), 0.0, 0.0));
    12. tang = normalize (float3 (1.0 - (norm.x * norm.x), 0.0, 0.0));
    13. tang = float3 (1.0, 0.0, 0.0);
    14.  
    15. // Except when norm.x is 1.0 or -1.0!
    16. // Let's use the norm.x = 1.0 here...
    17.  
    18. tang = normalize (float3 (1.0, 0.0, 0.0) - dot (float3 (1.0, 0.0, 0.0), norm) * norm);
    19. tang = normalize (float3 (1.0, 0.0, 0.0) - float3 (1.0, 0.0, 0.0) * norm);
    20. tang = normalize (float3 (1.0, 0.0, 0.0) - float3 (1.0 * 1.0, 0.0, 0.0));
    21. tang = normalize (float3 (1.0 - (1.0 * 1.0), 0.0, 0.0)); // norm.x is squared here, so it becomes 1.0 even if norm.x was originally -1.0
    22. tang = normalize (float3 (1.0 - 1.0, 0.0, 0.0));
    23. tang = normalize (float3 (0.0, 0.0, 0.0));
    24. // Vector length is now 0.0, you can't normalize because that involves a divide by zero.
    25. // Likely tangent winds up as a useless value of...
    26. tang = float3 (0.0, 0.0, 0.0);
     
  11. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    Very interesting indeed and major props for this in-depth investigation! So instead of the abs(norm.x) conditional I could alternatively (when first sampling from the norm g-buffer) do

    Code (csharp):
    1.  
    2. norm = half3(clamp(norm.x, -0.999, 0.999), norm.y, norm.z);
    3.  
    This "Gram-Schmidt" part also peaked my interest:

    Code (csharp):
    1.  
    2. tang = float3 (1.0, 0.0, 0.0);
    3. tang = normalize (tang - dot (tang, norm) * norm);
    4.  
    Is this like an optimization of cross knowing the one argument will be a unit vector?
     
  12. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    It's a standard formula for taking two arbitrary vectors and make one orthogonal to the other. In this case make "tang" orthogonal to "norm".

    vecA_orth_to_vecB = normalize (vecA - dot (vecA, vecB) * vecB);
     
  13. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    So then essentially I could replace both cross calls with this standard formula unless I'm mistaken. Gonna try this.

    Only "tangentially" related but I always wonder when doing a dot with a unit vector whether current shader compilers are smart enough to skip the "sum two mults-with-zero" implied in the dot. For readability, keeping the dot() call would be nice, but then I don't really want this to be actually computed uselessly at runtime..
     
  14. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    Well, you couldn't use it for both of them.

    You still need to use the cross product of normal & tangent to get the binormal because that ensures that the binormal is orthogonal to both the normal and the tangent, which is what you'll need to create a proper tangent space matrix. But you can use Gram-Scmidt to ensure that the tangent is orthogonal to the normal beforehand, then you know that the cross product of both of those will be orthogonal to both.
     
  15. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    You keep rocking this forum, Farfarer! Much obliged... and thanks for adding to my understanding!
     
  16. metaleap

    metaleap

    Joined:
    Oct 3, 2012
    Posts:
    589
    Interesting... cross produces subjectively speaking slightly more consistent and plausible anisotropic specular highlights, by-and-large favouring "vertical" streaks whereas Gram-Schmidt produces similar but "by-and-large horizontal" streaks (that's aniso whereas iso gives the well known round lobes only).

    So in front of a large shiny ground looking towards the rising or setting sun, "vertical" means a highlight in the middle going from the viewer towards the light source and "horizontal" means basically randomly all over the place but seemingly stretching from left to right. Or standing in front of a barrel that's facing the sun: "vertical" means the highlight stretches from its top to its bottom, "horizontal" means largely no highlight but at some angles it's again stretching from its "left" to its "right" when seen from the viewer..

    Guess since I am in a deferred screen-space ---as opposed to forward-rendering's object (or tangent) space--- I'll have to ultimately pick one or the other. Most gamers don't really care that much anyway, but so far I'm thinking I'm getting more pleasant and plausible highlights with cross (vertical)..