52
\$\begingroup\$

In my game there is a Minecraft-like terrain made out of cubes. I generate a vertex buffer from the voxel data and use a texture atlas for looks of different blocks:

texture atlas for voxel based terrain

The problem is that the texture of distant cubes interpolates with adjacent tiles in the texture atlas. That results in lines of wrong colors between cubes (you may need to view the screenshot below at its full size to see the graphical flaws):

screenshot of terrain with stripes of the wrong color

For now I use these interpolation settings but I tried every combination and even GL_NEAREST without mipmapping doesn't provide better results.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

I also tried added an offset in the texture coordinates to pick a slightly smaller area of the tile but since the unwanted effect depends on the distance to the camera this cannot solve the problem completely. At far distances the stripes occur anyway.

How can I solve this texture bleeding? Since using a texture atlas is popular technique there might be a common approach. Sadly, for some reasons explained in the comments, I cannot change to different textures or texture arrays.

\$\endgroup\$
20
  • 4
    \$\begingroup\$ The problem lies with mipmapping - your texture coordinates may work well for the biggest mip level, but as things get further away, the bordering tiles blend together. You can fight this by not using mipmaps (probably not a good idea), growing your guard zones (area between tiles), growing the guard zones dynamically in a shader based on mip level (tricky), do something strange while generating mipmaps (can't think of anything though).. I've never tackled this problem myself though, but there's stuff to start with.. =) \$\endgroup\$ Commented Jan 7, 2013 at 16:37
  • \$\begingroup\$ As I said sadly turning off mipmapping doesn't solve the problem. Without mipmaps it will either interpolate linear GL_LINEAR which gives a similar result or pick the nearest pixel GL_NEAREST which anyhow also results in stripes between blocks. Last mentioned option decreases but not eliminates the pixel flaw. \$\endgroup\$
    – danijar
    Commented Jan 7, 2013 at 16:44
  • 7
    \$\begingroup\$ One solution is to use a format that have generated mipmaps, then you could create thees mipmaps your self and correct the error. Another solution is to force a specific mipmap level in your fragmentshader when you are sampling the texture. Another solution is to fill the mipmaps with the first mipmap. In other words, when you are creating your texture, you can just add subimages to that particular image, containing the same data. \$\endgroup\$
    – Tordin
    Commented Jan 7, 2013 at 16:50
  • 1
    \$\begingroup\$ I had this problem before and it was solved by adding 1-2 pixel padding in your atlas between each different texture. \$\endgroup\$
    – Chuck D
    Commented Jan 8, 2013 at 6:01
  • 1
    \$\begingroup\$ @Kylotan. The problem is about texture resizing regardless of if it is done by mipmaps, linear interpolation or nearest pixel picking. \$\endgroup\$
    – danijar
    Commented Jan 10, 2013 at 20:34

5 Answers 5

41
\$\begingroup\$

Okay, I think you have two problems going on here.

The first problem is with mipmapping. In general, you don't want to naively mix atlasing with mipmapping, because unless all your subtextures are exactly 1x1 pixel sized, you will experience texture bleeding. Adding padding will simply move the problem to a lower mip level.

As a rule of thumb, for 2D, which is where you usually do atlasing, you don't mipmap; while for 3D, which is where you mipmap, you don't atlas (actually, you skin in such a way that bleeding won't be a problem)

However, what I think is going on with your program right now is that you're probably not using the correct texture coordinates, and because of that your textures are bleeding.

Let's suppose an 8x8 texture. You're probably getting the top-left quarter by using uv coordinates of (0,0) to (0.5, 0.5):

Incorrect mapping

And the problem is that at u-coordinate 0.5, the sampler will interpolate the destination fragment using half of the left texel and half of the right texel. The same will happen at u-coordinate 0, and the same for v-coordinates. This is because you're addressing the borders between texels, and not the centers.

What you should do instead is address the center of each texel. In this example, these are the coordinates you want to use:

Correct mapping

In this case, you will always be sampling the center of each texel and you should not get any bleeding... Unless you use mipmapping, in which case you will always get bleeding starting on the mip level where the scaled subtexture doesn't neatly fit inside the pixel grid.

To generalize, if you want to access a specific texel, the coordinates are calculated as:

function get_texel_coords(x, y, tex_width, tex_height)
    u = (x + 0.5) / tex_width
    v = (y + 0.5) / tex_height
    return u, v
end

This is called "half pixel correction". There's a more detailed explanation in here if you're interested.

\$\endgroup\$
26
  • \$\begingroup\$ Your explanation sounds very promising. Sadly since I have to video card at the moment, I cannot implement it yet. I will do so if the repaired card arrives. And I will calculate the mipmaps of the atlas on my own without interpolating over the boundaries of tiles then. \$\endgroup\$
    – danijar
    Commented Feb 18, 2013 at 14:20
  • 1
    \$\begingroup\$ @sharethis If you definitely have to atlas, make sure your subtextures have sizes in powers of 2, so you can limit bleeding to the lowest mip levels. If you do so, you don't really need to hand craft your own mipmaps. \$\endgroup\$ Commented Feb 18, 2013 at 14:30
  • \$\begingroup\$ Even PoT sized textures can have bleed artifacts though, because the bilinear filtering can sample across those boundaries. (At least in the implementations I've seen.) As for the half-pixel correction, should that apply in this case? If I wanted to map his rock texture for example, surely that will always be 0.0 to 0.25 along the U and V coordinates? \$\endgroup\$
    – Kylotan
    Commented Feb 18, 2013 at 17:31
  • 1
    \$\begingroup\$ @Kylotan If your texture size is even, and all the subtextures have even sizes and are located at even coordinates, bilinear filtering when halving the texture size will not mix between subtextures. Generalize for powers of two (if you're seeing artifacts, you're probably using bicubic, not bilinear filtering). Regarding half-pixel correction, it is necessary, because 0.25 is not the rightmost texel, but the middle point between both subtextures. To put it into perspective, imagine that you are the rasterizer: what is the color of the texture at (0.00, 0.25)? \$\endgroup\$ Commented Feb 19, 2013 at 1:59
  • 1
    \$\begingroup\$ @sharethis check out this other answer I wrote which may be able to help you gamedev.stackexchange.com/questions/50023/… \$\endgroup\$ Commented Mar 9, 2013 at 15:50
19
\$\begingroup\$

One option that'll be a lot easier than fiddling with mipmaps and adding texture coordinate fuzz factors is to use a texture array. Texture arrays are similar to 3d textures, but with no mipmapping in the 3rd dimension, so they're ideal for texture atlases where the "subtextures" are all the same size.

http://www.opengl.org/wiki/Array_Texture

\$\endgroup\$
8
  • \$\begingroup\$ If they're available, they're a much better solution - you get other features as well, like wrapping textures, that you miss with atlases. \$\endgroup\$ Commented Jan 7, 2013 at 17:01
  • \$\begingroup\$ @JariKomppa. I do not want to use texture arrays because that way I would have extra shaders for the terrain. Since the engine I write should be as general as possible I do not want to make this exception. Is there a way to create one shader that can handle both, texture arrays and single textures? \$\endgroup\$
    – danijar
    Commented Jan 10, 2013 at 20:47
  • 10
    \$\begingroup\$ @sharethis Dismissing unambiguously superiour technical solutions because you want to be "general" is a recipe for failure. If you write a game, don't write an engine. \$\endgroup\$ Commented Jan 11, 2013 at 12:13
  • 1
    \$\begingroup\$ @sharethis OK. I was somehow misled by the "In my game" part of the question. \$\endgroup\$ Commented Jan 11, 2013 at 13:23
  • 1
    \$\begingroup\$ @danijar Then you are deliberately limiting yourself. I used to have that perspective as well: that software must be built of generic components. But that is actually a recipe for inefficient software in many cases. What you actually want is to design the fastest code, and then figure out how to split up that code into components. Not design components and then figure out how to tape them together to make something fast. \$\endgroup\$
    – user20574
    Commented May 3, 2022 at 9:55
7
\$\begingroup\$

After struggling a lot with this issue, I finally came up with a solution.

To use both, a texture atlas and mipmapping, I need to perform the downsampling myself, because OpenGL would otherwise interpolate over the boundaries of tiles in the atlas. Moreover I needed to set the right texture parameters, because interpolating between mipmaps would also cause texture bleeding.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

With these parameters, no bleeding occurs.

There is one thing you have to notice when providing custom mipmaps. Since it doesn't make sense to shrink the atlas even if each tile is already 1x1, the maximal mipmap level must be set according to what you provide.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 7); // pick mipmap level 7 or lower

Thanks for all other answers and very helpful comments! By the way I still do not know how to use linear up scaling, but I guess there is no way.

\$\endgroup\$
5
  • \$\begingroup\$ good to know you got it solved. You should mark this answer as the correct one instead of mine. \$\endgroup\$ Commented Mar 11, 2013 at 3:40
  • \$\begingroup\$ mipmapping is a technique exclusively for downsampling, which makes no sense for upsampling. The idea is that if you're going to draw a small triangle, it would take a lot of time to sample a large texture just to get a few pixels out of it, so you pre-downsample it and use the appropriate mip level when drawing small triangles. For larger triangles, using anything different from the larger mip level doesn't make sense, so it is an error to do something like glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_FILTER, GL_NEAREST_MIPMAP_LINEAR); \$\endgroup\$ Commented Mar 11, 2013 at 3:52
  • \$\begingroup\$ @PandaPajama. You are right, I corrected my answer. To set GL_TEXTURE_MAX_FILTER to GL_NEAREST_MIPMAP_LINEAR causes bleeding not for the reason of mipmapping, but for the reason of interpolation. So in this case it is equivalent to GL_LINEAR since the lowest mipmap level is used anyway. \$\endgroup\$
    – danijar
    Commented Mar 11, 2013 at 12:06
  • \$\begingroup\$ @danijar - Can you explain in more detail how you perform the downsampling yourself, and how you tell OpenGL where (and when) to use the downsampled atlas? \$\endgroup\$
    – Tim Kane
    Commented Apr 27, 2014 at 19:26
  • 1
    \$\begingroup\$ @TimKane Split atlas into the tiles. For each mipmap level, downsample the tiles bilinearly, combine them, and upload that to the GPU specifying the mipmap level as parameter to glTexImage2D(). The GPU automatically chooses the correct level based on objects size on screen. \$\endgroup\$
    – danijar
    Commented Apr 27, 2014 at 20:55
5
\$\begingroup\$

Looks like the issue could be caused by MSAA. See this answer and the linked article:

"when you turn on MSAA, it then becomes possible for the shader to get executed for samples that are inside the pixel area, but outside of the triangle area"

The solution is to use centroid sampling. If you are calculating the wrapping of the texture coordinates in a vertex shader, moving that logic to the fragment shader could also fix the issue.

\$\endgroup\$
3
  • \$\begingroup\$ As I already wrote in another comment I have no working video card at the moment so I cannot check if that is the actual problem. I will do so as soon as I can. \$\endgroup\$
    – danijar
    Commented Feb 18, 2013 at 14:21
  • \$\begingroup\$ I disabled hardware antialiasing but the bleeding still remains. I already do the texture looking in the fragment shader. \$\endgroup\$
    – danijar
    Commented Mar 9, 2013 at 11:41
  • \$\begingroup\$ thanks! using glDisable(GL_MULTISAMPLE); solved my issue. \$\endgroup\$
    – Puddle
    Commented Jan 22, 2022 at 16:44
1
\$\begingroup\$

This is an old topic, and I had a similar problem to the topic.

I had my texture coordinates just fine, but by lerping the camera position (which changed my element's positions) like so:

public static void tween(Camera cam, Direction dir, Vec2 buffer, Vec2 start, Vec2 end) {
    if(step > steps) {
        tweening = false;
        return;
    }

    final float alpha = step/steps;

    switch(dir) {
    case UP:
    case DOWN:
        buffer = new Vec2(start.getX(), Util.lerp(start.getY(), (float)end.getY(), alpha));
        cam.getPosition().set(buffer);
        break;
    case LEFT:
    case RIGHT:
        buffer = new Vec2(Util.lerp(start.getX(), end.getX(), alpha), start.getY());
        cam.getPosition().set(buffer);
        break;
    }

    step += 1;
}

The whole problem was Util.lerp() returns a float or double. This was bad because the camera was translating off-grid and causing some weird issues with texture snippets that where > 0 in X and > 0 in Y.

TLDR: don't tween or anything using floats

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .