Jagged pixel lines with custom (texture atlas) shader

I’ve got a simple lambert shader and I have to modify it to work with atlases (for texture and normal maps), yet have it looking exactly the same. I did it and my atlas shader compiles and works, but these jaggies appeared around the edges of the blocks (I’m making a voxel based environment). Also, I’m not using any kind of antialiasing.

This shows the two shaders in action and the problem:

Pasted bellow is the code to my two shaders, I can’t find a clue on how to solve this so any contribution is much appreciated.

This is the original (simple lambert) shader:

Shader "Custom/TextureWithNormals"
{
    Properties{
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Texture", 2D) = "white" {}
        _BumpMap("Bumpmap", 2D) = "bump" {}
    }
        SubShader{
            Tags { "RenderType" = "Opaque" }
            LOD 200

            CGPROGRAM
            #pragma surface surf WrapLambert

            #pragma target 3.0

            half4 LightingWrapLambert(SurfaceOutput s, half3 lightDir, half atten)
            {
                half NdotL = dot(s.Normal, lightDir);
                half diff = NdotL * 0.5 + 0.5;
                half4 c;
                c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten);
                c.a = s.Alpha;
                return c;
            }

            struct Input
            {
                float2 uv_MainTex;
                float2 uv_BumpMap;
            };

            sampler2D _MainTex;
            sampler2D _BumpMap;
            fixed4 _Color;

            void surf(Input IN, inout SurfaceOutput o) {
                fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
                o.Alpha = c.a;
            }
            ENDCG
    }
        Fallback "Diffuse"
}

This is the atlas shader, based on the first one. AtlasX, AtlasY and AtlasRec are set by a script at Awake.
EDIT: Line 58 and 59 altered according to bgolus suggestion, jagged lines are now less noticiable but still present.

Shader "Custom/TexNormAtlas"
{
    Properties{
        _Color("Color", Color) = (1,1,1,1)
        _MainTex("Texture", 2D) = "white" {}
        _BumpMap("Bumpmap", 2D) = "bump" {}
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf WrapLambert
        #pragma vertex vert

        #pragma target 3.0

        sampler2D _MainTex;
        sampler2D _BumpMap;
        fixed4 _Color;

        half4 LightingWrapLambert(SurfaceOutput s, half3 lightDir, half atten)
        {
            half NdotL = dot(s.Normal, lightDir);
            half diff = NdotL * 0.5 + 0.5;
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten);
            c.a = s.Alpha;
            return c;
        }

        struct Input
        {
            float3 position;
            float4 custom_uv;
        };

        int _AtlasX;
        int _AtlasY;
        fixed4 _AtlasRec;

        void vert (inout appdata_full v, out Input o)
        {
            UNITY_INITIALIZE_OUTPUT(Input, o);
            o.custom_uv = v.texcoord;
            o.position = v.vertex;
        }

        void surf(Input IN, inout SurfaceOutput o) {
            fixed2 atlasOffset = IN.custom_uv.zw;
            fixed2 scaledUV = IN.custom_uv.xy;
            fixed2 atlasUV = scaledUV;
            atlasUV.x = (atlasOffset.x * _AtlasRec.x) + frac(atlasUV.x) * _AtlasRec.x;
            atlasUV.y = (((_AtlasY - 1) - atlasOffset.y) * _AtlasRec.y) + frac(atlasUV.y) * _AtlasRec.y;

            fixed4 c = tex2Dgrad(_MainTex, atlasUV, ddx(scaledUV * _AtlasRec), ddy(scaledUV * _AtlasRec)) * _Color;
            o.Normal = UnpackNormal(tex2Dgrad(_BumpMap, atlasUV, ddx(scaledUV * _AtlasRec), ddy(scaledUV * _AtlasRec)));

            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
        Fallback "Diffuse"
}

The point of using tex2Dgrad with ddx() / ddy() is to use the derivatives of the scaled UV without discontinuities caused by using a frac(). You’re getting the derivatives of the the already modified atlasUV, so you’re not going to get get anything better than you would have using tex2D. Plus you’re multiplying the already modified UVs by the scale a second time, which is going to produce even worse results than tex2D alone. I suspect those ddx() and ddy() lines were supposed to be using scaledUV and not atlasUV.

Hello bgolus, it’s always nice to have an answer by you. I’ve modified the code according to your suggestions and edited the question with the new code. There is a significant improvement, but not enough to get rid of the lines. Maybe I missed something?

This is a comparison between before/after your suggestion:

Now you’re up against the fundamental problem with texture atlasing. You need to add padding between your tiles or you get seams. Even if you’re using point filtering on the textures, because floating point math isn’t perfect, the UVs for an object can sample just outside of the range you specified, especially if you’re using MSAA which can end up over interpolating values. So instead of having the UV range for your tile go from one corner of the tile to the other, you need to have it inset by at least half a pixel. In situations where the textures are being point sampled, you can sometimes get away with clamping the UVs between half a texel and 1 - half a texel.

However the above only works if you disable mip maps. If you want mip mapping you will need a much wider padding, and also limit the mip map level since, which you can do by limiting the derivatives. Or if you’re generating the texture atlas via script, you can set a max mip level.

1 Like

Thank you so much bgolus, your answers are always full of great value. I found very little content about the subject so I’ll try to experiment with different inset values, if I get something better I will post the result here. Thanks for always helping everyone, you are a really great human being.