Tiling textures within an atlas by wrapping UVs within frag shader, getting artifacts

The problem is in how GPUs calculate which mip map to use.

The short version is GPUs calculate the texture’s mip map by how much the UVs change between one on screen pixel and the pixels next to it. This is done per quad of 2x2 pixels across the screen, per primitive coverage (i.e.: for any pixel quad each tri covers). This change of a value between pixels are known as pixel derivatives. When you’re using modulo (%) you’ll get a sudden break in the UVs where it jumps a large between two pixels, thus the GPU thinks it needs a lower mip map for that group of pixels. The easy solution is to disable mip maps, but that’s an ugly solution as the textures will alias under minification (when the texture is being displayed on screen at a lower resolution than the texture itself).

The two real solutions are calculating the mip map yourself and using tex2Dlod, or using tex2Dgrad and supplying your own derivatives. The second option sounds scary but is quite easy.

// starting uv
float2 uv = facing > 0 ? i.uv.xy : i.uv.zw;

// uv derivatives
float2 uv_ddx = ddx(uv);
float2 uv_ddy = ddy(uv);

// apply modulo wrap to uv
uv.x = (uv.x % (rect.z - _Padding * 2)) + rect.x + _Padding;
uv.y = (uv.y % (rect.w - _Padding * 2)) + rect.y + _Padding;

// sample texture supplying pre-wrapped uv derivatives
float4 sample = tex2Dgrad(_MainTex, uv, uv_ddx, uv_ddy);

Using tex2Dlod requires a bit more code to use, but the basic version is you use those same derivatives in a function to calculate the LOD using the same math the GPU would. This post has the relevant function, taken from the OpenGL documentation and written in GLSL, but the math is the same.

Note that the input texture_coordinate in that code snippet is the uv * texture resolution.

One nice advantage to tex2Dlod is you can easily clamp the mip level to prevent it from going too small, which is useful for when using an atlas, but you loose anisotropic filtering. You can actually do the same clamping with tex2Dgrad by limiting the length of the derivatives. Generally speaking unless you’re seeing significant artifacts on objects in the distance or at near edge on angles you can ignore this.

1 Like