Search Unity

Normal Map from Tangent Space to World

Discussion in 'Shaders' started by Serega4567, Jan 7, 2022.

  1. Serega4567

    Serega4567

    Joined:
    Jan 11, 2017
    Posts:
    23
    Hello.

    I am trying to write a surface shader with a Fresnel effect that takes the normal map into account.

    I started from this:

    Code (CSharp):
    1. Shader "Custom/TestShader"
    2. {
    3.     Properties
    4.     {
    5.         _NormalMap ("Normal Map", 2D) = "bump" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" }
    10.         LOD 200
    11.  
    12.         CGPROGRAM
    13.         #pragma surface surf Standard fullforwardshadows
    14.  
    15.         sampler2D _NormalMap;
    16.  
    17.         struct Input
    18.         {
    19.             float2 uv_NormalMap;
    20.         };
    21.  
    22.         void surf (Input IN, inout SurfaceOutputStandard o)
    23.         {
    24.             o.Albedo = o.Normal * 0.5 + 0.5;
    25.             o.Alpha = 1;
    26.         }
    27.         ENDCG
    28.     }
    29.     FallBack "Diffuse"
    30. }
    The surf function currently paints the object with the default world normals. Additionally, I multiply and add 0.5 to remap the resulting values from [-1; 1] to [0; 1]:

    first.png

    Now I change the assigned color in the surf function to a normal map:

    Code (CSharp):
    1. o.Albedo = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)) * 0.5 + 0.5;
    As expected, the result is in tangent space:

    second.png

    What should I do with the unpacked normal map in order to translate it into world space and preserve the relief? The result should look like this:

    9.png
     
  2. Invertex

    Invertex

    Joined:
    Nov 7, 2013
    Posts:
    1,550
    Apply the tangent to the vertex normal and then use
    mul(float4(adjustedSurfaceNormal, 0), unity_ObjectToWorld)
    to transform the direction to world space.
     
    Serega4567 likes this.
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    First, some weirdness with Surface Shaders you should know about. Whether or not you set
    o.Normal
    in the
    surf
    function drastically changes how some parts of the Surface Shader works.

    In your example shader you're using
    o.Normal
    to get the world normal. This only works if you don't assign
    o.Normal
    , as when you assign it the value expects to be set as a tangent space normal. If you add
    o.Normal = half3(0,0,1);
    to that shader the material will turn blue, even if you set
    o.Albedo
    before setting
    o.Normal
    . In fact it'll be blue even if you set
    o.Normal
    to something else because when you assign it the value then defaults to
    half3(0,0,1)
    .

    If you check out the documentation, at the bottom of the page there's a list of special
    Input
    struct values you can add that Unity will automatically fill in for you.
    https://docs.unity3d.com/Manual/SL-SurfaceShaders.html
    You'll notice a few of them have the extra note: "if surface shader does not write to o.Normal." As well as a few with the extra note showing what you need to do to get the world space vector for a normal.

    However that documentation incomplete. There's one more value on that list that changes when you set
    o.Normal
    .

    The
    float3 viewDir;
    is in world space if you don't set
    o.Normal
    , and will be in tangent space if it is. So if you want to get fresnel, add that variable to the
    Input
    struct, assign the o.Normal either with the tangent space normal from the texture or
    half3(0,0,1)
    if you don't want the normal map to affect lighting, and do a dot product between that and the tangent space normal.
     
    Serega4567 likes this.
  4. Serega4567

    Serega4567

    Joined:
    Jan 11, 2017
    Posts:
    23
    Thanks, it's strange that this feature of the viewDir property is not documented.

    I wrote in o.Normal the sampled normal map:

    Code (CSharp):
    1. o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
    Now, since both the normals and the view direction share the same coordinate system, I can calculate the dot product and finish the shader:

    1.png

    Of course, this solution suits me completely. I will focus on it as the shortest and simplest.

    Nevertheless, in order to close the thread issue, I would like to deal with the translation of the normal map into world space.

    Here on this page https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html some great examples of vertex-fragment shaders are provided. The section Environment reflection with a normal map solves exactly this problem. I took the second shader from this section, changed it a bit and got exactly the result that I originally wanted:

    2.png

    Here is the code I got this result with:

    Code (CSharp):
    1. Shader "Custom/TestVertexFragmentShader"
    2. {
    3.     Properties
    4.     {
    5.         _NormalMap("Normal Map", 2D) = "bump" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Pass
    10.         {
    11.             CGPROGRAM
    12.             #pragma vertex vert
    13.             #pragma fragment frag
    14.             #include "UnityCG.cginc"
    15.  
    16.             // normal map texture from shader properties
    17.             sampler2D _NormalMap;
    18.             // tiling and offset
    19.             float4 _NormalMap_ST;
    20.  
    21.             struct appdata
    22.             {
    23.                 float4 vertex : POSITION;
    24.                 float3 normal : NORMAL;
    25.                 float4 tangent : TANGENT;
    26.                 float2 uv : TEXCOORD0;
    27.             };
    28.  
    29.             struct v2f
    30.             {
    31.                 float3 worldPos : TEXCOORD0;
    32.                 // these three vectors will hold a 3x3 rotation matrix
    33.                 // that transforms from tangent to world space
    34.                 half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
    35.                 half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
    36.                 half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
    37.                 // texture coordinate for the normal map
    38.                 float2 uv : TEXCOORD4;
    39.                 float4 pos : SV_POSITION;
    40.             };
    41.  
    42.             // vertex shader now also needs a per-vertex tangent vector.
    43.             // in Unity tangents are 4D vectors, with the .w component used to
    44.             // indicate direction of the bitangent vector.
    45.             // we also need the texture coordinate.
    46.             v2f vert (appdata v)
    47.             {
    48.                 v2f o;
    49.                 o.pos = UnityObjectToClipPos(v.vertex);
    50.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    51.                 half3 wNormal = UnityObjectToWorldNormal(v.normal);
    52.                 half3 wTangent = UnityObjectToWorldDir(v.tangent.xyz);
    53.                 // compute bitangent from cross product of normal and tangent
    54.                 half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
    55.                 half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
    56.                 // output the tangent space matrix
    57.                 o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
    58.                 o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
    59.                 o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
    60.                 // apply tiling and offset
    61.                 o.uv = TRANSFORM_TEX(v.uv, _NormalMap);
    62.                 return o;
    63.             }
    64.        
    65.             fixed4 frag (v2f i) : SV_Target
    66.             {
    67.                 // sample the normal map, and decode from the Unity encoding
    68.                 half3 tnormal = UnpackNormal(tex2D(_NormalMap, i.uv));
    69.                 // !!! transform normal from tangent to world space
    70.                 half3 worldNormal;
    71.                 worldNormal.x = dot(i.tspace0, tnormal);
    72.                 worldNormal.y = dot(i.tspace1, tnormal);
    73.                 worldNormal.z = dot(i.tspace2, tnormal);
    74.                 // apply color
    75.                 fixed4 c = fixed4(worldNormal * 0.5 + 0.5, 1);
    76.                 return c;
    77.             }
    78.             ENDCG
    79.         }
    80.     }
    81. }
    Without hesitation, I defined a vert function in my original surface shader and copy-pasted the code there.

    Here's the result (it's darker because of ambient lighting):

    3.png

    And the code:

    Code (CSharp):
    1. Shader "Custom/TestShader"
    2. {
    3.     Properties
    4.     {
    5.         _NormalMap ("Normal Map", 2D) = "bump" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" }
    10.         LOD 200
    11.  
    12.         CGPROGRAM
    13.         #pragma surface surf Standard fullforwardshadows vertex:vert
    14.         #pragma target 3.5
    15.  
    16.         sampler2D _NormalMap;
    17.  
    18.         struct Input
    19.         {
    20.             float3 viewDir;
    21.             float2 uv_NormalMap;
    22.             half3 tspace0;
    23.             half3 tspace1;
    24.             half3 tspace2;
    25.         };
    26.  
    27.         void vert(inout appdata_full v, out Input o)
    28.         {
    29.             UNITY_INITIALIZE_OUTPUT(Input, o);
    30.             half3 wNormal = UnityObjectToWorldNormal(v.normal);
    31.             half3 wTangent = UnityObjectToWorldDir(v.tangent.xyz);
    32.             half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
    33.             half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
    34.             o.tspace0 = half3(wTangent.x, wBitangent.x, wNormal.x);
    35.             o.tspace1 = half3(wTangent.y, wBitangent.y, wNormal.y);
    36.             o.tspace2 = half3(wTangent.z, wBitangent.z, wNormal.z);
    37.         }
    38.  
    39.         void surf(Input IN, inout SurfaceOutputStandard o)
    40.         {
    41.             half3 tnormal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap));
    42.             half3 worldNormal;
    43.             worldNormal.x = dot(IN.tspace0, tnormal);
    44.             worldNormal.y = dot(IN.tspace1, tnormal);
    45.             worldNormal.z = dot(IN.tspace2, tnormal);
    46.            
    47.             o.Albedo = worldNormal * 0.5 + 0.5;
    48.             o.Alpha = 1;
    49.         }
    50.         ENDCG
    51.     }
    52.     FallBack "Diffuse"
    53. }
    As you can see, the result is identical, which is great. However, I'm not happy with the 3.5 model requirement. If I change the type of the argument in the vert function from appdata_full to appdata_tan, the 3.0 model will suffice, but I get the following warnings:

    Code (CSharp):
    1. Shader warning in 'Custom/TestShader': texcoord1 missing from surface shader's vertex input struct (appdata_tan), disabled lightmaps and meta pass generation
    2. Shader warning in 'Custom/TestShader': texcoord2 missing from surface shader's vertex input struct (appdata_tan), disabled dynamic GI and meta pass generation
    Once again, I appreciate everyone's help.
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    So, you did it the hard way. In my reply above:
    And here’s they very last point of the input struct list from the documentation I linked:
    Code (csharp):
    1. struct Input {
    2.   float2 uv_NormalMap;
    3.   float3 viewDir;
    4.   float3 worldNormal;
    5.   INTERNAL_DATA
    6. };
    7.  
    8. // in surf function
    9. float3 worldNormal = WorldNormalVector(IN, tnormal);
    10. o.Normal = tnormal;
    That’s it.


    There’s also a long running bug that in this use case,
    IN.worldNormal
    doesn’t have the world normal and is always
    float3(0,0,0)
    , so you have to use
    WorldNormalVector(IN, float3(0,0,1)
    to get the mesh’s world normal.
     
    Last edited: Jan 8, 2022
    p47p47 likes this.