Ugly seams between rotated normal mapped meshes

Hi
How to get rid of this unwanted ugly contrast between rotated meshes? (materials and meshes are same, just some meshes are rotated in 90, 180 and 270 degrees)

As you can see problem getting worse in complex normal maps:

this is my shader:

            v2f vert (appdata_full v, float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT )
            {
                v2f o;
                o.uv = v.texcoord;
                o.pos = UnityObjectToClipPos(vertex);
                 
                half3 normalDirection = UnityObjectToWorldNormal(normal);
                half3 wTangent = UnityObjectToWorldDir(tangent.xyz); 
                half tangentSign  = tangent.w + unity_WorldTransformParams.w;
                half3 wBitangent = cross( normalDirection, wTangent) + tangentSign;
                o.tspace0 = half3(wTangent.x, wBitangent.x, normalDirection.x);
                o.tspace1 = half3(wTangent.y, wBitangent.y, normalDirection.y);
                o.tspace2 = half3(wTangent.z, wBitangent.z, normalDirection.z); 
                return o;
            } 

            half4 frag (v2f i) : COLOR
            {
                half4 tnormal = tex2D(_BumpMap, i.uv);
                half3 normalMap = UnpackNormal(tnormal);

                half3 worldNormal;
                worldNormal.x = dot(i.tspace0, normalMap);
                worldNormal.y = dot(i.tspace1, normalMap);
                worldNormal.z = dot(i.tspace2, normalMap);
                worldNormal = normalize(worldNormal);
                   
                half nl = clamp(dot(worldNormal, _WorldSpaceLightPos0.xyz), 0.0, 1.0);

                half3 diffuse = half3(nl,nl,nl);
                diffuse =  diffuse * _LightColor0.rgb;
                half4 final = half4(diffuse, 1.0);
                return final;
            }

full shader is attached.

3264358–251832–normalMapTestShader.shader (2.33 KB)


half tangentSign = tangent.w + unity_WorldTransformParams.w;
half3 wBitangent = cross( normalDirection, wTangent) + tangentSign
Both should be a multiplication, not an addition.

1 Like

Wow! so fast reply,
but seems just getting inverted:(
3264385--251838--normal-map-seam3.jpg

Wow! so fast reply
you got lucky :slight_smile:

v2f vert (appdata_full v, float4 vertex : POSITION, float3 normal : NORMAL, float4 tangent : TANGENT )
Why do you declare additional parameters?
It’s all contained in appdata_full.
Try having it with just one input (the first parameter) and changing the code to use vertex, normal and tangent from ‘v’).

1 Like

no success, all the same:(:

Shader "#normalMapTestShader" {
    Properties {
        [NoScaleOffset] _BumpMap ("Bump Map", 2D) = "bump" {}
    }

   SubShader {
        Pass {
        LOD 400
        tags { "LightMode"="ForwardBase" "RenderType" = "Opaque" "PassFlags" = "OnlyDirectional"} 
            CGPROGRAM 
            #pragma vertex vert
            #pragma fragment frag
            #define UNITY_PASS_FORWARDBASE 

            #pragma glsl_no_auto_normalization
           
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #include "Lighting.cginc"
            struct v2f {
                half3 tspace0 : TEXCOORD1; // tangent.x, bitangent.x, normal.x
                half3 tspace1 : TEXCOORD2; // tangent.y, bitangent.y, normal.y
                half3 tspace2 : TEXCOORD3; // tangent.z, bitangent.z, normal.z
                // texture coordinate for the normal map
                float2 uv : TEXCOORD4;
                float4 pos : SV_POSITION;
            };
            
            uniform sampler2D _BumpMap;
        
            v2f vert (appdata_full v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                 
                half3 normalDirection = UnityObjectToWorldNormal(v.normal);
                half3 wTangent = UnityObjectToWorldDir(v.tangent.xyz); 
                half tangentSign  = v.tangent.w * unity_WorldTransformParams.w;
                half3 wBitangent = cross( normalDirection, wTangent) * tangentSign;
                o.tspace0 = half3(wTangent.x, wBitangent.x, normalDirection.x);
                o.tspace1 = half3(wTangent.y, wBitangent.y, normalDirection.y);
                o.tspace2 = half3(wTangent.z, wBitangent.z, normalDirection.z); 
  
                o.uv = v.texcoord;
                return o;
            } 

            half4 frag (v2f i) : COLOR
            {
                half4 tnormal = tex2D(_BumpMap, i.uv);
                half3 normalMap = UnpackNormal(tnormal);

                half3 worldNormal;
                worldNormal.x = dot(i.tspace0, normalMap);
                worldNormal.y = dot(i.tspace1, normalMap);
                worldNormal.z = dot(i.tspace2, normalMap);
                worldNormal = normalize(worldNormal);
                   
                half nl = clamp(dot(worldNormal, _WorldSpaceLightPos0.xyz), 0.0, 1.0);

                half3 diffuse = half3(nl,nl,nl);
                diffuse =  diffuse * _LightColor0.rgb;
                half4 final = half4(diffuse, 1.0);
                return final;
            }
            ENDCG
        }
    }
}

and where do these meshes come from?
do they have tangent information?

They are simple unity planes for test purposes.

OK, I don’t see anything wrong anymore…
What I try to do in such cases is visualise individual vectors and see where the discontinuity comes from.
E.g. output worldNormal from fragment shader, check if it looks the same everywhere. If it does, then the problem is not in the worldNormal or anything that is used to calculate it, it’s somewhere else. If it’s not consistent, check the inputs and so on, until you isolate which part is actually wrong.

1 Like

world normal:
3264575--251878--normal map world normal.jpg
wTangent: 3264575--251879--normal map wTangent.jpg
wBitangent: 3264575--251877--normal map wBitangent.jpg

any advice?!:roll_eyes:

This is world normal (something go wrong with previous one):

The real problem here has nothing to do with the shader unfortunately. It has everything to do with the normal maps themselves.

Normal maps are a normalized vector direction encoded into a texture. The x, y, and z of the vector has a range of -1.0 to 1.0 which is remapped to 0.0 to 1.0 to be stored in a texture. The problem is an 8 bit per channel normal map cannot store the vector 0.0, 0.0, 1.0, or “straight out”. Remapped that would be 0.5, 0.5, 1.0, but 127/255 = ~0.498 and 128/255 = ~0.502, so the resulting normal vectors are both slightly off of 0.0, 0.0, 1.0 (like +/-0.0039, +/-0.0039, 0.999). Those are both slightly off center. That means having several of them together and rotated will all look slightly different. That’s what you’re seeing with your simple test normal.

However the concrete normal map looks like it does just because it’s a bad normal map. The “flat” areas are around RGB 116, 137, 253, which is way off of the 127, 127, 255 or 128, 128, 255 that are close to the ideal.

1 Like

Oh my bad! you right, my normal maps entirely are bad normal maps, i have to fix them :(.
what we have to do about 8 bit per channel problem? is it possible to use 16 bit per channel texture? or using two 8 bit channels per axis? or someway changing any floats between 0.498 and 0.502 to 0.5?!
thank you very much, i forgot to say thank you to aleksandrk, thank you aleksandrk!

Dumb idea, but could you author your maps with slight reduction in range like 0 is -1 and 254 is+1 (instead of 255) then in the shader you just have to add a small constant to the x and y components you read from the normal texture to get 127 as 0?

It is possible to use 16 bit per channel textures, though by default Unity will still store them as 8 bit. I did some tests with using 8 bit or 16 bit per channel normal maps and found a 16 bpc normal map stored in the 8 bpc DXTnm (swizzled DXT5) still showed noticeable quality improvement, though not solving the “0” issue. Last I tested Unity had an issue with importing 16 bpc normals into a BC6H as it appeared to always assume the source texture was sRGB encoded. It would be possible to import your own 16 bit uncompressed textures, or to save out BC6H .dds files from an external application. It might even be possible to use a signed BC6H, but I’ve never tried.

Really a signed BC5 (two DXT5 alpha channels) generated from a 16 bpc source would be much better, but I’ve never worked with a game engine that supported that.

Not so dumb. Some people have used a scaling bias to make 0.498 = 0.5, but doing so means 128/255 is now even more obviously wrong which imposes some content creation limitations as you need to make sure your normal maps are generated with a program that either uses a negative bias or is aware of that rescaled range (would have to be custom) or doesn’t round up (some old programs did this, but they’ll also have much worse artifacts elsewhere). The other option is to do a more complex rescaling so that 0.498 to 0.502 range is all considered 0.5, but it can lead to some slight flattening in the resulting normals and adds a surprising amount of math.

norm.xy = tex2D(_BumpMap, uv).ag * 2.0 - 1.0; // what Unity does when using DXTnm
norm.xy = saturate(abs(norm.xy) * 1.003922 - 0.003922) * sign(norm.xy); // this is ~8 instructions!
norm.z = sqrt(1 - saturate(dot(norm.xy, norm.xy))); // reconstruct z

1 Like

Given OP already asked about switching to 16 bit normal textures, as if their tooling supports generating this, I was thinking a hack would be to author the textures as 16bit (enough precision to throw away a bunch going to 8 bit) and just make a crappy converter to quantise these to the modified 8 bit range (the extra 8 bits of throw away precision should be adequate I’d think)

Just need to read in the 16bit values, and scale, bias and round them, and write out to an 8bit image file

1 Like

Yep, that is an option. Even just naively converting 16 bit to 8 bit will effectively produce normals roughly centered around 127, as long as the shader handles the errant 255/255 value compression might reintroduce*, it would work.

float normScale = 255.0 / 254.0; // shader compiler will pre-calculate this
norm.xy = saturate(norm.xy * normScale) * 2.0 - 1.0;

A custom asset importer could plausibly be written for Unity as well, having it read the texture and do whatever it needs when imported as a Normal map. I haven’t looked into trying that though.

  • Note that the DXT5 texture format’s color data is limited to a 16 bit (5:6:5 bpc) color pallete end points, so 254/255 isn’t possible to encode as an end point. DXT block compression works by creating a pallete of 4 colors which has two end point colors and two mid point colors that are 33% and 67% blends of the two end points. Those mid points will be quantized to a full 24 bit (8:8:8 bpc) color so 254/255 can be encoded as a mid point value, but the compressor may chose to use 255/255 instead if there are too many other values in the 4x4 block.
1 Like