Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Tricky problem: calculating tangent.w on the fly?

Discussion in 'Shaders' started by Foxxis, Oct 6, 2014.

  1. Foxxis

    Foxxis

    Joined:
    Jun 27, 2006
    Posts:
    1,108
    Hi,

    So I have a relatively tricky problem. The community ocean shader calculates both fresnel and specular using a rotation matrix calculated in the shader:
    Code (CSharp):
    1. float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) );
    2.                 float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal );
    As you can see it omits multiplying the binormal with tangent.w (handedness) as tangent.w is used for foam (and w is not calculated for the wave meshes either as far as I can tell).

    Now, this "hack" works, as long as the mesh is not rotated. When it is rotated the rotation matrix seems to be incorrect for some verts leading to incorrect tiling when the normal map is used. I can see that flipping the binormal for these verts would solve the problem. However, I cannot figure out how to tell when to flip the binormal.
    Calculating tangent.w on the fly seems expensive. Is there some other trick that can be used?

    I would be immensely thankful for any help and the corrected shader will be uploaded to the community git. Thank you! :)
     
  2. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    If your water is like most water (roughly flat), then it should have no mirrored UVs at all, and thus all its tangent W components can safely be assumed to be 1.

    Are you sure the binormals are causing your issue?
     
  3. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    If the dot product of the binormal and the cross product of the normal and tangent is 0 or negative, multiply the binormal by -1.


    Code (csharp):
    1. if (dot (cross (normal, tangent), binormal) <= 0.0) {
    2. binormal = -binormal;
    3. }
     
  4. Daniel_Brauer

    Daniel_Brauer

    Unity Technologies

    Joined:
    Aug 11, 2006
    Posts:
    3,355
    Since binormal is defined as cross(N, T), that condition will always return false.

    I'm pretty sure it would be very difficult to determine whether UVs are mirrored (and thus binormals need flipping) in a shader. You need contextual information that is hard to come by at that level.

    As I said before, though, binormals should not need flipping for a large surface of water, even during gale-force winds.
     
  5. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    Ah, sorry, I skimmed over the fact that the normals and tangents weren't being generated on the fly too. You're correct.

    But yeah, you can generally safely assume that you're not going to need to flip binormals for flat planes of water (and if you do, it's safe to assume that it needs flipping everywhere, so you can just hard-code it in or even expose it as a shader parameter).
     
  6. Foxxis

    Foxxis

    Joined:
    Jun 27, 2006
    Posts:
    1,108
    Thanks for the replies!
    The thing is that I have verified that the shading anomaly *is* a binormal issue. The community ocean uses several tiles of non-flat meshes being generated on the fly each frame. We use a modified version of it, but the general principle is the same. The shader seems to have been written based on a water surface that is never rotated.

    The shader works perfectly well as long as the surfaces are not rotated around Y. When they are, the shading becomes non-uniform, ie. some tiles will render the normal map along one axis and adjacent tiles along the other. When I flip the binormal I can see that if I could flip just some of them (those that differ) the result would be perfect.
    But as I mentioned, I cannot figure out how to identify when I need to flip.

    The shader without modifications looks like this:
    Code (CSharp):
    1. //// Upgrade NOTE: now shader work on mobile
    2. // Upgrade NOTE: now shader have 2 lods, just disable reflection in inspector to see first lod
    3.  
    4. Shader "Mobile/Ocean"
    5. {
    6.     Properties
    7.     {
    8.         _Color ("Main Color", Color) = (1,1,1,1)
    9.         _SurfaceColor ("SurfaceColor", Color) = (1,1,1,1)
    10.         _WaterColor ("WaterColor", Color) = (1,1,1,1)
    11.         _Refraction ("Refraction (RGB)", 2D) = "white" {}
    12.         _Reflection ("Reflection (RGB)", 2D) = "white" {}
    13.         _Fresnel ("Fresnel (A) ", 2D) = "gray" {}
    14.         _Bump ("Bump (RGB)", 2D) = "bump" {}
    15.         _Foam ("Foam (RGB)", 2D) = "white" {}
    16.         _Size ("Size", Vector) = (1, 1, 1, 1)
    17.         _SunDir ("SunDir", Vector) = (0.3, -0.6, -1, 0)
    18.         _SpecColor ("SunColor", Color) = (0,0,0,1)
    19.      
    20.         _SurfaceColorLod1 ("SurfaceColor LOD1", Color) = (1,1,1,0.5)
    21.         _WaterColorLod1 ("WaterColor LOD1", Color) = (1,1,1,0.5)
    22.         _WaterTex ("Water LOD1 (RGB)", 2D) = "white" {}
    23.      
    24.         _Alpha ("Alpha", Range (0.01, 1)) = 0.9
    25.      
    26.     }
    27.     SubShader {
    28. //        Pass {  
    29. //         CGPROGRAM
    30. //
    31. //         #pragma vertex vert
    32. //         #pragma fragment frag
    33. //      
    34. //         uniform sampler2D _WaterTex;  
    35. //         uniform float4 _WaterTex_ST;
    36. //            // tiling and offset parameters of property          
    37. //
    38. //         struct vertexInput {
    39. //            float4 vertex : POSITION;
    40. //            float4 texcoord : TEXCOORD0;
    41. //         };
    42. //         struct vertexOutput {
    43. //            float4 pos : SV_POSITION;
    44. //            float4 tex : TEXCOORD0;
    45. //         };
    46. //
    47. //         vertexOutput vert(vertexInput input)
    48. //         {
    49. //            vertexOutput output;
    50. //
    51. //            output.tex = input.texcoord;
    52. //            output.pos = mul(UNITY_MATRIX_MVP, input.vertex);
    53. //            return output;
    54. //         }
    55. //
    56. //         float4 frag(vertexOutput input) : COLOR
    57. //         {
    58. //            return tex2D(_WaterTex,
    59. //               _WaterTex_ST.xy * input.tex.xy + _WaterTex_ST.zw);  
    60. //               // texture coordinates are multiplied with the tiling
    61. //               // parameters and the offset parameters are added
    62. //         }
    63. //
    64. //         ENDCG
    65. //      }
    66.  
    67.          Blend One OneMinusSrcAlpha
    68.         Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
    69.         //Tags { "RenderType"="Opaque" "RenderQueue"="Transparent" }
    70.         LOD 2
    71.         Pass {
    72.          
    73.             CGPROGRAM
    74.             #include "UnityCG.cginc"
    75.             #pragma exclude_renderers xbox360
    76.             #pragma vertex vert
    77.             #pragma fragment frag
    78.             #include "AutoLight.cginc"
    79.          
    80.             struct v2f
    81.             {
    82.                 float4 pos : SV_POSITION;
    83.                 float4  projTexCoord : TEXCOORD0;
    84.                 float2  bumpTexCoord : TEXCOORD1;
    85.                 float3  viewDir : TEXCOORD2;
    86.                 float3  objSpaceNormal : TEXCOORD3;
    87.                 float3  lightDir : TEXCOORD4;
    88.                 float2  foamStrengthAndDistance : TEXCOORD5;
    89.                 float4  lightCol : COLOR0;
    90.             };
    91.  
    92.             float4 _Size;
    93.             float4 _SunDir;
    94.             float4 _SpecColor;
    95.             half4 _SurfaceColor;
    96.             float4 _Color;
    97.             float _Alpha;
    98.          
    99.             v2f vert (appdata_tan v)
    100.             {
    101.                 v2f o;
    102.  
    103.                 o.bumpTexCoord.xy = v.vertex.xz/float2(_Size.x, _Size.z)*5;
    104.  
    105.                 o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
    106.  
    107.                 o.foamStrengthAndDistance.x = v.tangent.w;
    108.                 o.foamStrengthAndDistance.y = clamp(o.pos.z, 0, 1.0);
    109.  
    110.  
    111.                   float4 projSource = float4(v.vertex.x, 0.0, v.vertex.z, 1.0);
    112.                 float4 tmpProj = mul( UNITY_MATRIX_MVP, projSource);
    113.                 o.projTexCoord = tmpProj;
    114.  
    115.                 float3 objSpaceViewDir = ObjSpaceViewDir(v.vertex);
    116.                 float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) );
    117.                 float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal );
    118.  
    119.                 o.objSpaceNormal = v.normal;
    120.                 o.viewDir = mul(rotation, objSpaceViewDir);
    121.  
    122.                 o.lightDir = mul(rotation, float3(_SunDir.xyz));
    123.                 float3 lightDirection = mul(rotation, float3(_SunDir.xyz));
    124.                 o.lightCol = _SpecColor;
    125.                 return o;
    126.             }
    127.  
    128.             sampler2D _Refraction;
    129.             sampler2D _Reflection;
    130.             sampler2D _Fresnel;
    131.             sampler2D _Bump;
    132.             sampler2D _Foam;
    133.             half4 _WaterColor;
    134.  
    135.             half4 frag (v2f i) : COLOR
    136.             {
    137.                 half3 normViewDir = normalize(i.viewDir);
    138.  
    139.                 //half4 buv = half4(i.bumpTexCoord.x + _Time.x * 0.03, i.bumpTexCoord.y + _SinTime.x * 0.2, i.bumpTexCoord.x + _Time.y * 0.04, i.bumpTexCoord.y + _SinTime.y * 0.5);
    140.                 half4 buv = half4(i.bumpTexCoord.x + _Time.x * 0.003, i.bumpTexCoord.y + _SinTime.x * 0.1, i.bumpTexCoord.x + _Time.y * 0.04, i.bumpTexCoord.y + _SinTime.y * 0.25);
    141.  
    142.                 half3 tangentNormal0 = (tex2D(_Bump, buv.xy).rgb * 2.1) - 1;
    143.                 half3 tangentNormal1 = (tex2D(_Bump, buv.zw).rgb * 2.1) - 1;
    144.              
    145.                 half3 tangentNormal = normalize(tangentNormal0 + tangentNormal1);
    146.                 //tangentNormal = normalize(tangentNormal * i.bumpTexCoord.y);
    147.  
    148.                 float2 projTexCoord = 0.5 * i.projTexCoord.xy * float2(1, _ProjectionParams.x) / i.projTexCoord.w + float2(0.5, 0.5);
    149.  
    150.                 half4 result = half4(0, 0, 0, 1);
    151.  
    152.                 float2 bumpSampleOffset = i.objSpaceNormal.xz * 0.5 + tangentNormal.xy * 0.5;
    153.  
    154.                 half3 reflection = tex2D(_Reflection, projTexCoord.xy + bumpSampleOffset).rgb * _SurfaceColor.rgb;
    155.                 half3 refraction = tex2D(_Refraction, projTexCoord.xy + bumpSampleOffset).rgb * _WaterColor.rgb;
    156.  
    157.                 float fresnelLookup = dot(tangentNormal, normViewDir);
    158.  
    159.                 fixed bias = 0.06;
    160.                 half power = 4.0;
    161.                 half fresnelTerm = bias + (1.0-bias)*pow(1.0 - fresnelLookup, power);
    162.  
    163.                 half foamStrength = i.foamStrengthAndDistance.x * 1.8;
    164.  
    165.              
    166.                 half4 foam = clamp(tex2D(_Foam, i.bumpTexCoord.xy * 1.0)  - 0.5, 0.0, 1.0) * foamStrength;
    167.  
    168.                 float3 halfVec = normalize(normViewDir - normalize(i.lightDir));
    169.                 float specular = pow(max(dot(halfVec, tangentNormal.xyz), 0.0), 250.0);
    170.  
    171.                 result.rgb = lerp(refraction, reflection, fresnelTerm) + clamp(foam.r, 0.0, 1.0) + specular * _SpecColor;
    172.                 result.a = _Alpha;
    173.                 return result;
    174.             }
    175.             ENDCG
    176.         }
    177.     }

    By the way, I agree mirrored polys and winding are probably not the cause per se; the meshes are identical save for number of verts (LODing going on) and as far as I can tell they are constructed correctly. There seems to be something going amiss when using the calculated rotation matrix to generate a tangent space viewdir to query the normal map though.
    I have spent a few hours looking over the math, but sadly my linear algebra is a bit rusty...
    Again, many thanks in advance for any help or hints!
     
    Last edited: Oct 7, 2014
  7. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    So there's a of things in that code I can see.

    When building the rotation matrix, you need to multiply the binormal by tangent.w - it's precomputed for you if you're using a mesh. Unity has a macro that builds it for you.

    TANGENT_SPACE_ROTATION;

    Secondly, you can't blend normal maps just by multiplying them together. Check out this for the best results; http://bblog.selfshadow.com/publications/blending-in-detail/
     
  8. Foxxis

    Foxxis

    Joined:
    Jun 27, 2006
    Posts:
    1,108
    Why?
    As far as I know, you multiply by tangent.w to flip the binormal to account for handedness. That is not the issue here as far as I can tell. The rotation of the mesh introduces the shading error.
    With all due respect, please do explain how any proposed fixes relate to this particular problem.
     
  9. Foxxis

    Foxxis

    Joined:
    Jun 27, 2006
    Posts:
    1,108
  10. Farfarer

    Farfarer

    Joined:
    Aug 17, 2010
    Posts:
    2,249
    Not multiplied, sorry, I mean added - was looking at that on my phone and it's hard to read. This is the bit I meant.

    Code (csharp):
    1. half3 tangentNormal0 = (tex2D(_Bump, buv.xy).rgb * 2.1) - 1;
    2. half3 tangentNormal1 = (tex2D(_Bump, buv.zw).rgb * 2.1) - 1;
    3. half3 tangentNormal = normalize(tangentNormal0 + tangentNormal1);
    Also they're being unpacked strangely (and incorrectly). Why * 2.1 - 1? Should be * 2 - 1. Unity has macros for this which will work correctly on mobile and desktop stuff, because normal maps are compressed differently on those platforms.

    Try this instead:
    Code (csharp):
    1. fixed3 tangentNormal0 = UnpackNormal(tex2D(_Bump, buv.xy)) + fixed3(0, 0, 1);
    2. fixed3 tangentNormal1 = UnpackNormal(tex2D(_Bump, buv.zw)) * fixed3(-1, -1, 1);
    3. fixed3 tangentNormal = fixed3(tangentNormal0 * dot(tangentNormal0, tangentNormal1) / tangentNormal0.z - tangentNormal1);
     
  11. Foxxis

    Foxxis

    Joined:
    Jun 27, 2006
    Posts:
    1,108
    Thanks!
    I have no idea why it is written the way it is, nor who wrote it. It is part of the community ocean package (on the wiki). I think someone has tried to write an optimised version for mobile platforms and has taken a bunch of shortcuts to get it to perform better.
    I suspect one of those shortcuts may be why it is shading inconsistently when rotated.

    Would you care to elaborate on your remark regarding multiplying the binormal with tangent.w? Tangent.w is used by the ocean system as foam info rather than as a flip bit. As mentioned I suspect flipping should be done based on some other factor than handedness (as the poly winding should be identical).

    TIA!