Search Unity

Noob Question: Why do we need to convert Normal Maps from tangent space?

Discussion in 'Shaders' started by Tset_Tsyung, Sep 26, 2019.

  1. Tset_Tsyung

    Tset_Tsyung

    Joined:
    Jan 12, 2016
    Posts:
    411
    Hey all,

    This is NOT a rant, but rather a request for help to spot the flaws in my understanding and where I need to go and study more.

    So, yes, this is a noob question, however it's one I haven't been able to find and answer for on Google. They say that the Normal Maps are in tangent space, and they need to be converted... but aren't Albedo textures ALSO in tangent space, and yet we don't convert those...

    So why do we need to convert from tangent space?

    Allow me to illustrate my question:

    I have this normal map (from a wiki site):
    free-normal-map-scale-leather_800x800.jpg

    And I'm applying it to a quad using this shader (Stripped right back to help illustrate my question):
    Code (CSharp):
    1. Shader "DiffuseNormalMap"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.         _BumpMap("Normal Map", 2D) = "bump" {}
    7.     }
    8.     SubShader
    9.     {
    10.         Tags { "LightMode" = "ForwardBase" }
    11.         LOD 100
    12.  
    13.         Pass
    14.         {
    15.             CGPROGRAM
    16.             #pragma vertex vert
    17.             #pragma fragment frag
    18.  
    19.             #include "UnityCG.cginc"
    20.  
    21.             struct appdata
    22.             {
    23.                 float4 vertex : POSITION;
    24.                 float3 normal: NORMAL;
    25.                 float2 uv : TEXCOORD0;
    26.             };
    27.  
    28.             struct v2f
    29.             {
    30.                 float4 vertex : SV_POSITION;
    31.                 float2 uv : TEXCOORD0;
    32.                 float3 worldNormal : TEXCOORD1;
    33.             };
    34.  
    35.             uniform sampler2D _MainTex;
    36.             uniform sampler2D _BumpMap;
    37.             uniform float4 _MainTex_ST;
    38.             uniform float4 _LightColor0;
    39.  
    40.             v2f vert (appdata v)
    41.             {
    42.                 v2f o;
    43.                 o.vertex = UnityObjectToClipPos(v.vertex);
    44.                 o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    45.                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
    46.                 return o;
    47.             }
    48.  
    49.             fixed4 frag (v2f i) : SV_Target
    50.             {
    51.                 // Simply unpacking the normal and adding it the the vertex normal (normalized for interpolation)
    52.                 float3 normalNormal = UnpackNormal(tex2D(_BumpMap,i.uv));
    53.                 float4 diffuse = _LightColor0 * max(0.0, dot((normalize(i.worldNormal)+normalNormal), _WorldSpaceLightPos0.xyz));
    54.                 diffuse += UNITY_LIGHTMODEL_AMBIENT;
    55.                 fixed4 col = diffuse * tex2D(_MainTex, i.uv);
    56.                 return col;
    57.             }
    58.             ENDCG
    59.         }
    60.     }
    61. }
    62.  
    And here's the result (You can see the direction of the light source):
    NormalLIghting.PNG

    So, it lights up okay. I rotate the quad or the light source and it seems to light up okay. So, why do I need to perform any more calculations? What problems would be encountered if not converted from tangent space?

    I have looked into several pages and have tried to wrap my head around the 'why', but they just seem to jump straight into the 'how' - unfortunately I learn better when I know the why first... :(

    I'm learning shading from Unity's reference and the awesome WikiBooks site (https://en.wikibooks.org/wiki/Cg_Programming/Unity/Lighting_of_Bumpy_Surfaces) but still can't find an answer to my question, I've even tried https://learnopengl.com/Advanced-Lighting/Normal-Mapping but still no luck :(

    As always I appreciate any and all help - many thanks! XD

    NOTE: the lighting of the bump map seems to be reversed, is THIS the reason for the tangent space conversion?
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    To answer this question we need to answer some other questions.

    Lets first answer the question of "what is a normal" in this context.
    A normal is unit length directional vector. Vector means it has multiple components, in this case 3; x, y, and z. Directional means just that, it's a direction. Unit length means its length is 1. The most important part of that is that it is a mathematical representation of a direction. Imagine it as a line pointing out from a surface saying "I'm facing this way".

    Next, what is tangent space? Or maybe before that, let's talk about space conversions in general.
    Think about a mesh's vertex positions. Those start out in object space (aka mesh or local space) relative to the position of the mesh's pivot. When you move a mesh around in the scene, you're moving it in world space. To get the vertex positions from object space to world space so it appears in the correct position you need to transform the object space vertex positions into world space positions. For simple translation only movement (moving without any rotation or scale) this is as simple as adding the offset to the vertex positions. For rotation you need to do more complex matrix math that applies the rotation to the vertex positions around the object space pivot before offsetting the positions. That probably all makes sense... maybe not the matrix math itself, but the need to rotate and move something to move between spaces.

    For directional vectors you only want to apply the rotation, and not the position or scale. This is where that "unit length" thing comes into play. You can apply a transform matrix without the position offset trivially, but the rotation and scale are baked together. Luckily since directional vectors are always unit length, you can normalize the vector (make it unit length again by getting the current length and dividing the vector by that length) after rotating it. Unit length vectors are also useful for a lot of math as it simplifies comparing directions against each other using dot products, which are really cheap. Most lighting calculations are based off of dot products.

    When doing lighting, we need to know the surface normal and the lighting direction in the same space. If you were to use the mesh's object space normal and compare it to the world space light direction, then when you rotate your mesh the lighting won't match what you expect it to be. So you first have to translate both the vertex normals (and positions if using point lights) from object space to world space. Now you can compare the world space lighting direction and the now world space normal using a dot product, and you have your Lambert diffuse shading model.

    So what about tangent space? Tangent space is interesting because it's effectively only rotational in application. It's the orientation of the surface normal and the "flow" of the UVs. I do a better job of explaining tangent space in my article on triplanar mapping, here:
    https://medium.com/@bgolus/normal-mapping-for-a-triplanar-shader-10bf39dca05a#0576

    But this image sums up a lot of it.

    If you repeat this texture on your mesh, it's showing you that mesh's tangent space orientation. Z is the normal vector orientation (imaging it's protruding out), X is the tangent vector orientation, and Y is the bitangent / binormal vector orientation (sort of, see the above article for more on that).

    Normal maps come in several flavors depending on the space they're representing, but the most common form is tangent space normal maps, so most of the time when someone says "normal map" they really mean "tangent space normal map". It's a representation of a normal vector in tangent space, encoded into a texture (-1 to 1 range scaled into a 0 to 1, aka 0/255 to 255/255 range). So now to do lighting, you need both the normal direction in the normal map and the lighting direction to be in the same space, just like with vertex normals. So we transform the tangent space normal vector into world space.

    Technically you could also transform the light direction into tangent space, but sometimes you want access to other data when lighting, and that's often already transformed into world space for other reasons, so it's more efficient to get the normal in world space rather than the other way around. Plus the tangent space transforms are potentially unique to each rendered pixel of a mesh, where as world space is consistent, which is why light positions and directions start in in world space to begin with.

    With a sprite or the basic quad with no rotation, the orientation of the tangent space is pretty close to world space already. All the axis are already aligned, but not necessarily facing the same directions. This is why the lighting looks almost correct when not transforming from tangent to world before doing the lighting calculations. Technically if you're doing a 2D game where you're never going to rotate a sprite, you can get away with inverting the normal map's Z and it'll match world space. But any rotation and they won't match. You can try that right now by rotating your quad and you'll see the lighting doesn't change.

    Albedo textures aren't in any space*. They aren't a representation of a position or direction, they're just a color value. They don't need to be rotated or translated when an object moves, the movement of the vertex positions and the subsequent interpolation of the UVs across those transformed triangles do all the work for you.

    * Albedo textures are technically in sRGB color space, but color space is another topic.
     
    exitshadow, hbguler, daoth90 and 5 others like this.
  3. Tset_Tsyung

    Tset_Tsyung

    Joined:
    Jan 12, 2016
    Posts:
    411
    Hey @bgolus,

    Wow, thank you for taking the time to write such a thorough explanation - I have come across your posts in the past and noted that your replies are always informative and appreciated. Many thanks.

    Okay, so everything in the first 5 paragraphs I thankfully already knew (phew), and paras 6 and 7 I had juuuuust about wrapped my head around this afternoon ('UnpackNormals' or 'tex2D() *2 -1')... but with this next quote you really nailed home the points I was struggling with.

    Which is where I was going wrong for so long! I was only working in two dimensions, hence not understanding why tangent space conversion was necessary!

    After I posted this question, and after a lot of faffing and rotating and re-diting the shader, I managed to get it to work properly (in all 3 dimensions), but still didn't understand why it was working. This sentence just helped everything fall into place! Thank you so so so so much!

    I think I've gotten the grasp on it now, but can I ask if the following is correct?

    Tangent space could be described as a TRIANGLE LOCAL space, which means that the tangent direction (given by vertex semantics) and the bitangent (cross product of normal and tangent) are converted to world space so that when we apply the 'tangent space normal maps' values to the vertex normal the lighting model gives the correct illusion of extra geometry in the right place and direction.

    Is that correct?

    Again, thank you so much for your reply - you've really saved my sanity and keyboard.

    Mike
     
    Last edited: Sep 26, 2019
  4. Tset_Tsyung

    Tset_Tsyung

    Joined:
    Jan 12, 2016
    Posts:
    411
    Ha ha ha, thank you for not pushing me too hard. Although I did learn (through rereading wikibooks page for nth time) that Unity packs 2 vectors of the needed 3 into 2 channels (G and A), and that the third vector data is extrapolated as the missing data from a normal value (1) - I thought this was rather clever. But I digress... and am just trying to make myself sound less of a dumbass, ha ha ha.
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Hmm...

    It is not really “triangle local”, but pixel local. The tangent to world matrix changes over the surface of the triangle as the vertex data is interpolated. For a “flat” triangle where the vertex normals and tangents for all 3 vertices are identical, then the matrix will be the same across the surface, but that’s more an edge case than anything consistent.

    You’re also not “applying” the normal map to the vertex normal, the vertex normal is being used to construct the rotation matrix that transforms the tangent space into world space. It’d be more accurate to say that you’re applying the vertex normal to the normal map.

    Really something like a normalized rotation matrix (one that has no scale) is a set of unit length vectors that describe what direction x, y and z are compared to the space it is transforming from. If you read up on 2D rotation matrixes you might be able to get your head around that pretty easily, 3D rotation works exactly the same way. Once you get passed the explanations of imaginary numbers and extra dimensional space that many of the tutorials dive into...

    I might also quibble over the phrase “illusion of extra geometry” and say it’s the illusion of extra surface detail, but that would just me being pedantic rather being helpful.

    Yep, this is actually one of the big reasons why we use tangent space normal maps instead of object or world space so often. It’s also why there are several two channel compressed texture formats, like BC5, which Unity can now use for normals instead of swizzled DXT5. Some companies still use DXT5 textures packed similarly to what Unity does, and uses the unused channels to store additional data. This comes with some quality loss, both in the encoded normals and the extra data, but it means one less texture to store and sample from.

    Tangent space normal maps also tend to hide the limited precision of the 8 bit per channel texture formats we usually use, as a smooth sphere will still look smooth with a normal map since most of the “sphere-ness” is coming from the interpolated vertex normals and not the normal map, where as an object space normal map the sphere might looks a little “crunchy”.
     
    hbguler likes this.
  6. Tset_Tsyung

    Tset_Tsyung

    Joined:
    Jan 12, 2016
    Posts:
    411
    Right, got it ;)


    Yeah... I've been putting off the study of the 3D maths needed in games and graphics for long enough... think I'll start that today and get a proper intuitive understanding of it all...


    THANK YOU! My brain had totally shut down last night, I was thinking 'detail'... but geometry is all I could think of, lmao.

    P.S. I sometimes fond pedantic is actually rather helpful, ;)

    Again, many thanks for taking the time to help me with this. All the best in your current and future projects.

    Mike