Unity3D visible seams on borders when tiling texture
Asked Answered
E

2

5

For my game I have written a shader that allows my texture to tile nicely over multiple objects. I do that by choosing the uv not based on the relative position of the vertex, but on the absolute world position. The custom shader is as follows. Basically it just tiles the texture in a grid of 1x1 world units.

Shader "MyGame/Tile" 
{
    Properties 
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader 
    { 
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert
        sampler2D _MainTex;

        struct Input 
        {
            float2 uv_MainTex;
            float3 worldPos;
        };

        void surf (Input IN, inout SurfaceOutput o) 
        {
            //adjust UV for tiling
            float2 cell = floor(IN.worldPos.xz);
            float2 offset = IN.worldPos.xz - cell;
            float2 uv = offset;

            float4 mainTex = tex2D(_MainTex, uv);
            o.Albedo = mainTex.rgb;
        }
        ENDCG
    } 
    FallBack "Diffuse"
}

I have done this approach in Cg and in HLSL shaders on XNA before and it always worked like a charm. With the Unity shader, however, I get a very visible seam on the edges of the texture. I tried a Unity surface shader as well as a vertex/fragment shader, both with the same results.

visible seams

The texture itself looks as follows. In my game it is actually a .tga, not a .png, but that doesn't cause the problem. The problem occurs on all texture filter settings and on repeat or clamp mode equally.

texture

Now I've seen someone have a similar problem here: Seams between planes when lightmapping. There was, however, no definitive answer on how to solve such a problem. Also, my problem doesn't relate to a lightmap or lighting at all. In the fragment shader I tested, there was no lighting enabled and the issue was still present.

The same question was also posted on the Unity answers site, but I received no answers and not a lot of views, so I am trying it here as well: Visible seams on borders when tiling texture

Emulsoid answered 3/4, 2013 at 8:52 Comment(5)
On your texture, try changing Wrap Mode to "Clamp".Cutter
As I stated: The problem occurs on all texture filter settings and on repeat or clamp mode equally.Emulsoid
Ack, I grep'd for 'wrap mode', not clamp.Cutter
Be sure to play with MipMaps as well. See if that helps.Descriptive
You don't need to implement frac yourself. http.developer.nvidia.com/Cg/frac.htmlCant
C
6

This describes the reason for your problem: http://hacksoflife.blogspot.com/2011/01/derivatives-i-discontinuities-and.html

This is a great visual example, like yours: http://aras-p.info/blog/2010/01/07/screenspace-vs-mip-mapping/

Unless you're going to disable mipmaps, I don't think this is solvable with Unity, because as far as I know, it won't let you use functions that let you specify what mip level to use in the fragment shader (at least on OS X / OpenGL ES; this might not be a problem if you're only targeting Windows).

That said, I have no idea why you're doing the fragment-level uv calculations that you are; just passing data from the vertex shader works just fine, with a tileable texture:

struct v2f {
    float4 position_clip : SV_POSITION;
    float2 position_world_xz : TEXCOORD;
};

#pragma vertex vert
v2f vert(float4 vertex : POSITION) {
    v2f o;
    o.position_clip = mul(UNITY_MATRIX_MVP, vertex);
    o.position_world_xz = mul(_Object2World, vertex).xz;
    return o;
}

#pragma fragment frag
uniform sampler2D _MainTex;
fixed4 frag(v2f i) : COLOR {
    return tex2D(_MainTex, i.position_world_xz);
}
Cant answered 3/4, 2013 at 15:51 Comment(1)
Thanks a lot, I don't know why I didn't think of this myself. Of course I can just take the world position from the vertex-shader without further calculations. This actually makes the problem disappear (disabling mip-maps also worked).Emulsoid
C
0

The existing answer is fine for a situation where you can do it in another way, but Unity actually does make it possible to solve this while keeping the existing coordinates ‒ for example I was in a situation where the actual texture was taken from a texture "atlas" where it has borders, but it should also be repeatable, so I used frac to do any tiling myself and then faced this issue.

The reason this artifact happens is partly due to how pixels are rendered ‒ they are not rendered individually, but in a 2×2 quad, which is why you might see this spanning 2×2 squares of pixels. The other half of the reason are mipmaps ‒ usually a single texture in memory does not consist of just one image, but alongisde multiple scaled-down (halved) versions of the image called mipmaps, which are selected based on the LOD (level-of-detail). As a part of its magic, tex2D automatically computes the LOD based on the local resolution of the texture as seen on the screen, picking higher LODs (and thus smaller mipmaps) when the individual pixels of the texture are very close together (in either directions). This is called anisotropic filtering and helps both optimization (the larger texture does not have to be sampled often) and visuals.

This magic of tex2D involves computing the derivative of uv alongside both axes, which is functionally equivalent to calling ddx(uv) and ddy(uv). You can compute the LOD yourself when you call those two functions, multiple them by the texture's dimensions, pick the maximum, and take base-2 logarithm of it (each mipmap has halved size of the larger one). Then you may call tex2Dlod and give it the LOD manually.

However, you don't have to compute the level of detail yourself! tex2D actually offers an overload taking dx and dy which are the aforementioned derivatives, tex2D(s, uv) being equivalent to tex2D(s, uv, ddx(uv), ddy(uv)). How is this useful? The reason the seams are visible is due to how the derivatives are computed ‒ the GPU simply checks the neighboring pixel in the same quad (based on the used axis) and takes the difference of the neighbor's value of uv and the current pixel's value (a discrete derivative is just difference). It is now obvious why those pixels look the way they look ‒ while the derivative is usually nice and small everywhere else, at the seams it is huge because suddenly the values jump around. Since the derivative is huge, the GPU thinks such a small difference in screen space is actually a very large difference in the UV space, thus the pixels must be really crammed together, and so it loads a smaller (maybe the smallest) mipmap, which is usually just the original texture blurred together.

To solve this issue in a very neat way, I suggest using two sets of uv ‒ one to do the actual sampling, the other to compute the derivatives of. You may freely perform any operation on the "real" uv, but forbid yourself from doing any discontinuous operation on the second pair. Feel free to do scaling (really the only important operation here), translation etc., but not frac or % or similar operations. For example (equivalent to your original code):

float2 uvbase = IN.worldPos.xz;
float2 uv = frac(uvbase);
float4 mainTex = tex2D(_MainTex, uv);

can be changed to

float2 uvbase = IN.worldPos.xz;
float2 uv = frac(uvbase);
float4 mainTex = tex2D(_MainTex, uv, ddx(uvbase), ddy(uvbase));

frac does not introduce any scaling to uvbase, and so most of the time, the derivatives will be equal to those of uv, but they will also work as expected on the seams.

Consuetudinary answered 16/8, 2023 at 22:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.