Ending The Confusion About Mirrored Normal Mapping

It’s time to finally squash this issue I’ve been having for years. I’ll admit, I’m a bit confused about what’s actually going on, and it’s hard to track down the cause of all this, but I’ll do my best to explain. Also, I’ve seen quite a few other posts about this so it’s probably best to at least gather info so others can avoid these complications.

The Problem:

Mirrored objects end up with incorrect normal mapping on the mirrored side. Example:

Note that the right side is correctly “bumped” yet the left is essentially an inverted version of the normal map.

Here’s another example, probably more noticeable:

It should be noted that I’m using Blender 2.75 for all of my modeling.

What’s happening?

After extensive searching I’ve found a few possibilities as to what this could be.

  1. Tangents [w] are inverted on the mirrored side
  2. Normals are somehow inverted on the mirrored side
  3. The very nature of normal maps in Tangent space doesn’t allow for a mirrored UV map

I thought it was certainly the first item listed, due to my findings on this post. However, I recently attempted that solution for the models above, and they remain the way seen above. (There is a visible change, but it appears to only flip the problem to the other side)

Is there a way to fix this?

I’d like to use the original Blender file (for workflow purposes) as the main Asset for my game. That means I’d prefer not to have to export it as any other format. I’d also like to keep the mirroring on my objects and my UVs the way they are. Any of you other developers would certainly know that keeping the mirror has a tremendous advantage when changes need to be made to the model. And diffuse maps can be made much faster. Certainly modern 3D programs wouldn’t stop us from having a mirrored normal map right?

Here’s a few links that add even more confusion to the issue.

In this post, Daniel claims that Unity calculates the tangents correctly for mirrored meshes. Later on, he says that it could be an issue with other file formats or software. Could Blender be not exporting the “homogeneous tangents” needed to display the normal map properly? Or is Unity grabbing the wrong information from the native Blender file?

This post has doj, who says Unity doesn’t handle mirrored tangents correctly.

This user recommends triangulation. I’ve tried this, and it didn’t help the issue above.

Help is appreciated! Thanks!

EDIT: I’m using Blender’s Mirror modifier. If you’re unfamiliar with this mesh modifier, it essentially duplicates geometry on a specific axis and updates in realtime. In my files, the modifier has not been “applied,” meaning I can’t directly edit the mirrored side. Blender is intended to do this work instead.

When an object is mirrored. Every information is inverted specially normal is obvious.
Simply freeze transformation / Reset X-form after mirroring and flip faces should be OK.

I’m not too sure about this, it’s my understanding that Blender’s Mirror Modifier handles normals correctly. See this post. The user claims the issue is present from manual mirroring, but with the built-in modifier results are correct.

I’ve also verified this myself, take a look at the object’s normals with it’s mirror shown in edit mode:

I think I should also specify that I’m using Blender’s mirror modifier. “After mirroring” is not something that happens on the user end because it’s specifically designed to update the mirrored side in realtime. This is different from Maya and other software in the fact that the software handles all the mirroring automatically. So long as the mirror modifier is present, you’ll only ever have to deal with half the mesh. As stated above, I’d to keep it this way for workflow purposes. OP has been edited to clarify.

Oh. You means like Symmetry (3dsMax) modifier. Okay. Then it should be the normal map itself.
Need some touch-up skills. A bit hard to explain but you can take a look on this:

Also. Make sure those triangulation (Hidden lines) between mirror line are same.

It isn’t just the seams though–if you look at the second picture in my first post, you can see the whole mirrored side is inverted. Sure, I could blend the seams so there’s no visible mark at the mirror point, but that won’t solve the fact that the entire normal map (on that side) is showing up inverted. While it may not be noticeable with hand-painted maps, baked maps end up with entire sections of the same direction.

At the end of the tutorial above, the user says it does not fix the “inverted lighting bug” on mirrored geometry. That sounds closer to what’s happening here. Here’s a forum on Kerbal Space Program that just confused me even more about this stuff.

So it’s a problem with the normal map and not the geometry?
I guess the normal aren’t inverted so much as one component is inverted, you should try to verify that.

With a custom shader, I’ve been able to verify that the tangents are in deed “out of whack.” Results:

And here’s the normals as they appear in Unity:

For anyone interested, here’s the shader I made to Debug this. Simply comment out the o.color with the other one to check normals.

Debug Tangent Shader

Shader "Custom/TangentDebug" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
       
        Pass {
        CGPROGRAM
        #pragma vertex vert
          #pragma fragment frag
          #include "UnityCG.cginc"
        
    
      struct v2f {
          float4 pos : SV_POSITION;
          fixed4 color : COLOR;
      };
     
      v2f vert (appdata_tan v)
      {
          v2f o;
          o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
          float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w;
         
          //o.color.xyz = v.normal * 0.5 + 0.5;
          o.color.xyz = binormal * 0.8;
          o.color.w = 1.0;
          return o;
      }

      fixed4 frag (v2f i) : SV_Target { return i.color; }
      ENDCG
      }
    }
    FallBack "Diffuse"
   
}

Now the issue at hand… Why is Unity handling the tangents this way? The tangents are set to “calculate” in the import settings. (Normals are imported) Can anyone explain what’s going on?!

Tangents look correct actually.

That looks correct?! The big black and red divide down the middle is what looks off to me. Could you elaborate?

Tangents and binormals are in the direction of the texture UV “flow”, so if a model is mirrored I would expect the tangents or binormals to have a hard break.

The red and black make sense since red is a vector pointed toward x (1,0,0), and black is likely negative x (-1,0,0).

The binormal shouldn’t affect the normal anyway, right? If binormal is wack it affect UV I think.

If you are showing the normal relative to surface, it should be a straight blue (0,0,1), I don’t see you translating the normal to worldspace so why are the model colored?

keeping my eyes on this, I have this problem too…
I always ended up in having to uv map the mirrored side too…

Tangents and binormals are 100% related to normals, specifically normal maps, that’s the only reason they exist. They have no effect on the UVs themselves; tangents and binormals are based on the UVs, but the UVs don’t care about them or even if they exist.

Don’t conflate surface normals and normal maps. Normal maps are in “tangent space”, and would appear blue if there was no detail in them, but surface normals are initially in model space which is going to be pointed in all sorts of directions unless you have a flat plane.

A tangent space normal map stores a direction relative to the surface tangents, that is to say the red is the right or left amount, green is the up or down amount, and blue is the forward or back amount, but those directions are relative to the orientation of how the texture appears. Most people understand surface normals as the direction away from the surface, but tangents and binormals are usually more difficult for people to understand.


If you were to use this crappy image as the texture on your model the arrows point (roughly) in the direction of the tangent (X) and binormal (Y).

I need to sleep to make stupid error like this …

Thanks for your explanation, bgolus. I think I understand how tangents, normals, and the UV0 map are related now. This article also helped. If I’m understanding correctly, it makes sense that since that the “red” left and right are inverted, since the “U” value of the UV coordinates is mirrored.

As the article I linked mentions,

Now the ultimate problem is… Is there any way around this? I’m referring back to this post for this information.

In Summary

  • Normals are correct on mirrored geometry

  • UV’s are mirrored (as they should be)

  • Tangents on the mirrored side are incorrect; they are pointing inwards to the model

  • The Bitangents on the mirrored side that Unity calculates are correct, due to this:

  • float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w;

  • Inverted the Tangents on the mirrored side (fixing them) will cause an incorrect bitangent, as their W component will need to be inverted (unless they are calculated as homogenous tangents)

Help on the execution for this fix would be appreciated. Perhaps Blender has a way of declaring how a mesh’s tangents are handled?

I’ve found a temporary solution.

Setting the tangents to None in the model importer seems to do the trick. The results look a bit different then when they were on “Calculate,” but it looks fine for my purposes. Here’s a few theories for what’s happening (correct me if I’m wrong)

  • Unity’s Standard shader has an Object Space normal map fallback OR

  • The Shader calculates it’s own tangents when none is provided in the model OR

  • “None” actually means that Blender somehow passes correct data to Unity

I’m thinking number 1 or 2 is the case because I don’t remember this working on the legacy shaders. Either way, I’m not about to complain or question anything. If anyone has a more correct explanation as to why this is producing this result, I’m all ears. And if number 1 is indeed the case, this should really be listed in the docs or made clear for those that want a good fallback for mirrored geometry.

Tangents set to none should disable normal maps entirely, or be using completely bogus data (maybe just always a single object space direction?). The standard shader absolutely does not have an object space fallback, and does not calculate its own tangents, and unless there’s a bug “None” shouldn’t read anything from blender at all.

edit: confirmed setting it to “none” results in constant tangent of (1,0,0,1). Also, the issue happens with a model from 3ds Max as well so it’s not confined to blender.

edit 2: I take that back, it was a shader bug causing it for the 3ds Max model.

Here’s a shader I use to test this kind of stuff. Lets you switch between several different modes. Displays mesh normals, tangents, and bitangents (aka binormals) in world space, as well as the normal mapped normals in world space. Enabling derivatives calculates the tangent and binormal directly in the shader rather than using the tangent from the mesh. They’ll be “chunkier” than mesh tangents, but can be used to validate the correctness of the tangents and binormals.

Shader "Tangent Visualizer" {
Properties {
    _MainTex ("Base (RGB)", 2D) = "white" {}
    [NoScaleOffset] _BumpMap ("Normal Map", 2D) = "bump" {}
    [KeywordEnum(Lit, Mesh Normals, Mesh Tangents, Mesh Bitangents, Tangent Normals, World Normals)] _Display ("Debug Display", Float) = 0
    [Toggle(_DERIVATIVES_ON)] _Derivatives ("Debug Use Derivatives", Float) = 0
    [Enum(Zero to One, 0, Normalized, 1, Inverted, 2, Absolute, 3)] _Normalization ("Vector Display", Float) = 0
}

SubShader {
    Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" }
    LOD 100
   
    Pass {  
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma shader_feature _ _DISPLAY_MESH_NORMALS _DISPLAY_MESH_TANGENTS _DISPLAY_MESH_BITANGENTS _DISPLAY_TANGENT_NORMALS _DISPLAY_WORLD_NORMALS
            #pragma shader_feature _ _DERIVATIVES_ON
           
            #include "UnityCG.cginc"

            struct v2f {
                float4 vertex : SV_POSITION;
                half2 texcoord : TEXCOORD0;
                #ifdef _DERIVATIVES_ON
                half3 normal : TEXCOORD1;
                float3 pos : TEXCOORD2;
                #else
                half3x3 tspace : TEXCOORD1;
                #endif
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _BumpMap;

            fixed4 _LightColor0;
            float _Normalization;

            float3x3 cotangent_frame( float3 normal, float3 position, float2 uv )
            {
                // get edge vectors of the pixel triangle
                float3 dp1 = ddx( position );
                float3 dp2 = ddy( position ) * _ProjectionParams.x;
                float2 duv1 = ddx( uv );
                float2 duv2 = ddy( uv ) * _ProjectionParams.x;
             
                // solve the linear system
                float3 dp2perp = cross( dp2, normal );
                float3 dp1perp = cross( normal, dp1 );
                float3 T = dp2perp * duv1.x + dp1perp * duv2.x;
                float3 B = dp2perp * duv1.y + dp1perp * duv2.y;
             
                // construct a scale-invariant frame 
                // float invmax = rsqrt( max( dot(T,T), dot(B,B) ) );
                // return transpose(float3x3( T * invmax, B * invmax, normal ));
                return transpose(float3x3( normalize(T), normalize(B), normal ));
            }

            fixed4 rescaleForVis(half3 vec)
            {
                vec = normalize(vec);
                if (_Normalization == 3.0)
                    return fixed4(abs(vec), 1.0);
                if (_Normalization == 2.0)
                    return fixed4(-vec, 1.0);
                if (_Normalization == 1.0)
                    return fixed4(vec, 1.0);
                // if (_Normalization == 0.0)
                    return fixed4(vec * 0.5 + 0.5, 1.0);
            }
                       
            v2f vert (appdata_full v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

        #ifdef _DERIVATIVES_ON
                o.normal = UnityObjectToWorldNormal(v.normal);
                o.pos = mul(_Object2World, v.vertex);
        #else
                half3 wNormal = UnityObjectToWorldNormal(v.normal);
                half3 wTangent = UnityObjectToWorldDir(v.tangent.xyz);
                // compute bitangent from cross product of normal and tangent
                half tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                half3 wBitangent = cross(wNormal, wTangent) * tangentSign;
                // output the tangent space matrix
                o.tspace = half3x3(wTangent.x, wBitangent.x, wNormal.x,
                                   wTangent.y, wBitangent.y, wNormal.y,
                                   wTangent.z, wBitangent.z, wNormal.z);
        #endif
                return o;
            }
           
            fixed4 frag (v2f i) : SV_Target
            {
                half3x3 tbn;
            #ifdef _DERIVATIVES_ON
                tbn = cotangent_frame(i.normal, i.pos, i.texcoord);
            #else
                tbn = i.tspace;
            #endif

            #ifdef _DISPLAY_MESH_NORMALS
                return rescaleForVis(half3(tbn[0].z, tbn[1].z, tbn[2].z));
            #endif

            #ifdef _DISPLAY_MESH_TANGENTS
                return rescaleForVis(half3(tbn[0].x, tbn[1].x, tbn[2].x));
            #endif

            #ifdef _DISPLAY_MESH_BITANGENTS
                return rescaleForVis(half3(tbn[0].y, tbn[1].y, tbn[2].y));
            #endif

                half3 tnormal = UnpackNormal(tex2D(_BumpMap, i.texcoord));

            #ifdef _DISPLAY_TANGENT_NORMALS
                return rescaleForVis(tnormal);
            #endif

                half3 worldNormal = normalize(mul(tbn, tnormal));

            #ifdef _DISPLAY_WORLD_NORMALS
                return rescaleForVis(worldNormal);
            #endif

                fixed4 col = tex2D(_MainTex, i.texcoord);
                half ndotl = dot(worldNormal, _WorldSpaceLightPos0.xyz);
                col.rgb *= saturate(ndotl) * _LightColor0.rgb + ShadeSH9 (float4(worldNormal,1.0));

                return col;
            }
        ENDCG
    }
}

}

The shader was throwing an error “undeclared identifier” for unity_WorldTransformParams. Any ideas?

So the whole “constant tangent” is a bug? Or is it a bug that the 3ds Max model was doing that? Also, thanks for your time on this!