Search Unity

Multiple tangent spaces?

Discussion in 'Shaders' started by bluescrn, Apr 1, 2022.

  1. bluescrn

    bluescrn

    Joined:
    Feb 25, 2013
    Posts:
    642
    Has anyone created a shader that deals with 2 tangent spaces to blend normal maps that are differently oriented?

    We want to use UV0 for a base map, and UV1 to add details (decals/trim) that also have a normal map. The problem is that the orientation of the mapping isn't guaranteed to be the same between the two layers.

    Is there a standard nice for this sort of case? Or do people avoid it?

    I guess I could use an AssetPostprocessor script to generate the second set of tangents, or some data derived from them (the 2D rotation required to rotate one normal into the other tangent space?).

    (And on that subject, is there a better way than naming convention/string matching to tag models that require special postprocessing? - Is there any way to add custom data/UI to the import settings?)
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    Yes. I have.

    Generally people avoid it, or they just YOLO it and use the UV0 tangents for everything (Unity's own shaders do this!). None of Unity's systems know how to handle more than one per vertex tangent, so things like batching or skinning will break if you try and generate a second set of tangents as they won't get transformed properly. Though the idea of using a tangent that's rotated from the base is a decent solution to that.

    However what I do is I generate the tangent to world matrix in the fragment shader.

    Code (csharp):
    1. // Unity version of http://www.thetenthplanet.de/archives/1180
    2. float3x3 cotangent_frame( float3 normal, float3 position, float2 uv )
    3. {
    4.     // get edge vectors of the pixel triangle
    5.     float3 dp1 = ddx( position );
    6.     float3 dp2 = ddy( position ) * _ProjectionParams.x;
    7.     float2 duv1 = ddx( uv );
    8.     float2 duv2 = ddy( uv ) * _ProjectionParams.x;
    9.     // solve the linear system
    10.     float3 dp2perp = cross( dp2, normal );
    11.     float3 dp1perp = cross( normal, dp1 );
    12.     float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    13.     float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
    14.     // construct a scale-invariant frame
    15.     float invmax = rsqrt( max( dot(T,T), dot(B,B) ) );
    16.     return transpose(float3x3( T * invmax, B * invmax, normal ));
    17. }
     
  3. one_one

    one_one

    Joined:
    May 20, 2013
    Posts:
    621
    Thank you for the link to that post and for converting that snippet! For those (like me) who aren't adept enough with graphics to immediately know how to utilize it in a context as described by @bluescrn :
    • The
      normal
      parameter is the mesh's (interpolated) normal in world space
    • The
      position
      parameter is the world space position of the fragment minus the camera position. If you use HDRP with camera-relative rendering, this will simply be the world space position (the camera is at (0,0,0) then.)
    • You utilize your normal map vector by multiplying it with the cotangent frame matrix. By doing so, you perturb the mesh normal via your normal map and receive a normal in world space.
    • If you use shader graph or if you want to blend normals together from maps with different UV layouts (as was asked above with trim sheets) , you'll want to transform this normal from world to tangent space.
     
    lilacsky824 likes this.
  4. DJ_piercy

    DJ_piercy

    Joined:
    Dec 29, 2022
    Posts:
    2
    I have been banging my head against the wall for the past 3 hours, and for the life of me, I can't figure out how to properly integrate this into my shader. I have no clue what I'm doing wrong here, and I hate to ask, but would you mind looking at my code and pointing out my mistakes?

    Code (CSharp):
    1. Shader "Custom/Trim sheets"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Base color", 2D) = "white" {}
    6. [Normal]_MainNorm ("Base normal", 2D) = "bump" {}
    7.     _MainRM ("Base Roughness (R) and Metalic (G)", 2D) = "grey" {}
    8.     _Trim ("Trim color", 2D) = "white" {}
    9. [Normal]_TrimNorm ("Trim normal", 2D) = "bump" {}
    10.     _MainRM ("Trim Roughness (R) and Metalic (G)", 2D) = "grey" {}
    11.  
    12.     }
    13.     SubShader
    14.     {
    15.         Tags { "RenderType"="Opaque" "PerformanceChecks"="False"}
    16.         LOD 200
    17.  
    18.         CGPROGRAM
    19.         #pragma surface surf Standard fullforwardshadows vertex:vert
    20.         #pragma target 3.5
    21.  
    22.         sampler2D _MainTex;
    23.         sampler2D _MainNorm;
    24.         sampler2D _MainRM;
    25.     sampler2D _Trim;
    26.         sampler2D _TrimNorm;
    27.         sampler2D _TrimRM;
    28.  
    29.         struct Input
    30.         {
    31.                 float2 uv_MainTex : TEXCOORD0;
    32.         float2 uv3_Trim : TEXCOORD2;
    33.         float4 pos;
    34.         float3 norm;
    35.         };
    36.  
    37. void vert (inout appdata_full v, out Input o) {
    38.     UNITY_INITIALIZE_OUTPUT(Input,o);
    39.     o.norm = v.normal;
    40.     o.pos = UnityObjectToClipPos(v.vertex);
    41.  
    42. }
    43.         UNITY_INSTANCING_BUFFER_START(Props)
    44.         UNITY_INSTANCING_BUFFER_END(Props)
    45.  
    46.  
    47.  
    48.         void surf (Input IN, inout SurfaceOutputStandard o)
    49.         {
    50.  
    51.  
    52. //your code starts here
    53.     float3 dp1 = ddx( IN.pos );
    54.     float3 dp2 = ddy( IN.pos ) * _ProjectionParams.x;
    55.     float2 duv1 = ddx( IN.uv3_Trim );
    56.     float2 duv2 = ddy( IN.uv3_Trim ) * _ProjectionParams.x;
    57.     // solve the linear system
    58.     float3 dp2perp = cross( dp2, IN.norm );
    59.     float3 dp1perp = cross( IN.norm, dp1 );
    60.     float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    61.     float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
    62.     // construct a scale-invariant frame
    63.     float invmax = rsqrt( max( dot(T,T), dot(B,B) ) );
    64.  
    65.     float3x3 cotangent_frame = transpose(float3x3( T * invmax, B * invmax, IN.norm ));
    66. //your code ends here
    67.  
    68.  
    69.     float3 twonorm = tex2D (_TrimNorm, IN.uv3_Trim);
    70.     twonorm*=2;
    71.     twonorm-=1;
    72.     float3 Norm2 =normalize(mul(cotangent_frame,twonorm));
    73.     o.Normal = Norm2;
    74.  
    75.  
    76.  
    77.             fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
    78.         o.Albedo = c.rgb;
    79.                o.Alpha = c.a;
    80.         }
    81.         ENDCG
    82.     }
    83.     FallBack "Diffuse"
    84. }
    85.  
    Here's how it works currently. my shader is on the left, and a reference using the standard shader using one uv map is on the right. I think it might be misidentifying the normals, because the error gets worse when I rotate the object.

     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,343
    The position should be the world position, not the clip space position. You could use
    float3 worldPos;
    in the
    Input
    struct, or to avoid some precision issues that will cause noise, use this in the vert program:

    Code (csharp):
    1. o.pos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1)).xyz - _WorldSpaceCameraPos;
    The other problem is this will calculate the tangent to world transform for an arbitrary UV. But the
    o.Normals
    expects the tangent space normals using the tangents for the mesh’s base UVs. So you still need to transform the world normals back into the’s mesh’s tangent space.

    For that you’ll want to use this function:
    https://github.com/bgolus/Normal-Ma...blob/master/TriplanarSurfaceShader.shader#L63
     
    Last edited: Dec 30, 2022
  6. DJ_piercy

    DJ_piercy

    Joined:
    Dec 29, 2022
    Posts:
    2
    Thank you, it works perfectly now. For anyone in the future that finds this thread and needs something similar, here's the final shader:

    edit: It works fine so long as you don't rotate the model in question. If anyone can figure out how to fix that, please do share.

    Code (CSharp):
    1. Shader "Custom/Trim sheets"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Base Color", 2D) = "white" {}
    6. [Normal]_MainNorm ("Base Normal", 2D) = "bump" {}
    7.     _MainR ("Base Roughness", 2D) = "grey" {}
    8.     _MainM ("Base Metalic", 2D) = "black" {}
    9.     _Trim ("Trim Color", 2D) = "black" {}
    10. [Normal]_TrimNorm ("Trim Normal", 2D) = "bump" {}
    11.     _TrimR ("Trim Roughness", 2D) = "grey" {}
    12.     _TrimM ("Trim Metalic", 2D) = "black" {}
    13.  
    14.     }
    15.     SubShader
    16.     {
    17.         Tags { "RenderType"="Opaque" "PerformanceChecks"="False"}
    18.         LOD 200
    19.  
    20.         CGPROGRAM
    21.         // Physically based Standard lighting model, and enable shadows on all light types
    22.         #pragma surface surf Standard fullforwardshadows vertex:vert
    23.  
    24.         // Use shader model 3.5 target, because otherwise there won't be enough texture space.
    25.         #pragma target 3.5
    26.  
    27.     #include "UnityStandardUtils.cginc"
    28.  
    29.  
    30.         sampler2D _MainTex;
    31.         sampler2D _MainNorm;
    32.         sampler2D _MainR;
    33.     sampler2D _MainM;
    34.     sampler2D _Trim;
    35.         sampler2D _TrimNorm;
    36.         sampler2D _TrimR;
    37.     sampler2D _TrimM;
    38.  
    39.         struct Input
    40.         {
    41.                 float2 uv_MainTex : TEXCOORD0;
    42.         float2 uv3_Trim : TEXCOORD2;
    43.         float3 pos;
    44.         float3 norm;
    45.         float3 worldNormal;
    46. INTERNAL_DATA
    47.         };
    48.  
    49. float3 WorldToTangentNormalVector(Input IN, float3 normal) {
    50.             float3 t2w0 = WorldNormalVector(IN, float3(1,0,0));
    51.             float3 t2w1 = WorldNormalVector(IN, float3(0,1,0));
    52.             float3 t2w2 = WorldNormalVector(IN, float3(0,0,1));
    53.             float3x3 t2w = float3x3(t2w0, t2w1, t2w2);
    54.             return normalize(mul(t2w, normal));
    55.         }
    56.  
    57. void vert (inout appdata_full v, out Input o) {
    58.     UNITY_INITIALIZE_OUTPUT(Input,o);
    59.     o.norm = v.normal;
    60. o.pos = mul(unity_ObjectToWorld, float4(v.vertex.xyz, 1)).xyz - _WorldSpaceCameraPos;
    61.  
    62.  
    63. }
    64.         // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
    65.         // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
    66.         // #pragma instancing_options assumeuniformscaling
    67.         UNITY_INSTANCING_BUFFER_START(Props)
    68.             // put more per-instance properties here
    69.         UNITY_INSTANCING_BUFFER_END(Props)
    70.  
    71.  
    72.  
    73.         void surf (Input IN, inout SurfaceOutputStandard o)
    74.         {
    75.     IN.worldNormal = WorldNormalVector(IN, float3(0,0,1));
    76.     fixed4 c;
    77.  
    78. float4 trim = tex2D (_Trim, IN.uv3_Trim);
    79.  
    80. //if you need to use a different normal map, switch the IN.uv variable
    81.     float3 dp1 = ddx( IN.pos );
    82.     float3 dp2 = ddy( IN.pos ) * _ProjectionParams.x;
    83.     float2 duv1 = ddx( IN.uv3_Trim );
    84.     float2 duv2 = ddy( IN.uv3_Trim ) * _ProjectionParams.x;
    85.     // solve the linear system
    86.     float3 dp2perp = cross( dp2, IN.norm );
    87.     float3 dp1perp = cross( IN.norm, dp1 );
    88.     float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    89.     float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
    90.     // construct a scale-invariant frame
    91.     float invmax = rsqrt( max( dot(T,T), dot(B,B) ) );
    92.  
    93.     float3x3 cotangent_frame = transpose(float3x3( T * invmax, B * invmax, IN.norm ));
    94.  
    95.  
    96.     float3 twonorm = tex2D (_TrimNorm, IN.uv3_Trim);
    97.     twonorm*=2;
    98.     twonorm-=1;
    99.     float3 Norm2 =normalize(mul(cotangent_frame,twonorm));
    100.  
    101. o.Normal=normalize(lerp(UnpackNormal(tex2D(_MainNorm, IN.uv_MainTex)),WorldToTangentNormalVector(IN, Norm2),trim.a));
    102. c = lerp( tex2D (_MainTex, IN.uv_MainTex),trim.rgba,trim.a);
    103. o.Smoothness = 1-lerp( tex2D (_MainR, IN.uv_MainTex), tex2D (_TrimR, IN.uv3_Trim),trim.a);
    104. o.Metallic = lerp( tex2D (_MainM, IN.uv_MainTex), tex2D (_TrimM, IN.uv3_Trim),trim.a);
    105.  
    106.         o.Albedo = c.rgb;
    107.                o.Alpha = c.a;
    108.         }
    109.         ENDCG
    110.     }
    111.     FallBack "Diffuse"
    112. }
    113.  
     
    Last edited: Dec 31, 2022