Symptom
Suppose I want to display tex1 in the upper-left corner of an image and tex2 in the lower-right corner. Both textures use trilinear sampling, but their sampling coordinates differ. The code is as follows:
void main() { vec4 color = vec4(0.0); vec2 st = uv; if (uv.x < uv.y) { st -= 0.5; // Cause of mipmap bleeding color = texture(tex1, st); } else { color = texture(tex2, st); } gl_FragColor = color; }
This code produces the following artifact:

The artifact disappears when mipmapping is disabled, which confirms that it is caused by mipmap sampling.
Fortunately, pema wrote an article about mipmaps a few months ago: Mipmap selection in too much detail. It is an excellent article, and I highly recommend reading it.
I cannot help feeling a little jealous whenever I come across content this good. I wish I could write something like it 🌚.
Cause
The root of the problem lies in how mipmaps are sampled. We commonly sample a hardware mipmap like this:
vec4 color = texture(tex, uv);
A software equivalent would look like this:
float level(vec2 uv, vec2 res) { vec2 dx = dFdx(uv) * res; vec2 dy = dFdy(uv) * res; float d = max(dot(dx, dx), dot(dy, dy)); return max(0.0, 0.5 * log2(d)); } vec4 color = textureLod(tex, uv, level(uv, TEXTURE_SIZE));
As we can see, dFdx and dFdy are used to calculate the mipmap level. Looking back at the code above:
if (uv.x < uv.y) { st -= 0.5; // Cause of mipmap bleeding color = texture(tex1, st); } else { color = texture(tex2, st); }
Because we modify st inside the if statement, dFdx(st) and dFdy(st) become large when neighboring fragments take different branches.
This increases the calculated mipmap level, causing the texture lookup to sample a lower-resolution mipmap.
In extreme cases, it may even sample the smallest mipmap. Since that mipmap is a solid-colored 1px x 1px texture,
a line appears near the pixels where the jump occurs.
We can construct a mipmap whose smallest level is a cyan square. The line becomes cyan, confirming that the smallest mipmap is being sampled:

Solution
We can solve this problem by preventing the sampling coordinates from jumping between branches. Move the coordinate modifications outside the conditional branch so that the values of dFdx and dFdy remain unchanged:
void main() { vec4 color = vec4(0.0); vec2 st1 = uv - 0.5; vec2 st2 = uv; if (uv.x < uv.y) { color = texture(tex1, st1); } else { color = texture(tex2, st2); } gl_FragColor = color; }