Sunday, September 9, 2018

opengl - Seamless tilemap rendering (borderless adjacent images)


I have a 2D game engine that draws tilemaps by drawing tiles from a tileset image. Because by default OpenGL can only wrap the entire texture (GL_REPEAT), and not just part of it, each tile is split off in to a separate texture. Then regions of the same tile are rendered adjacent to each other. Here's what it looks like when it's working as intended:


Tilemap with no seams


However as soon as you introduce fractional scaling, seams appear:


Tilemap with seams


Why does this happen? I thought it was due to linear filtering blending the borders of the quads, but it still happens with point filtering. The only solution I've found so far is to ensure all positioning and scaling only happens at integer values, and use point filtering. This can degrade the visual quality of the game (particularly that sub-pixel positioning no longer works so motion is not so smooth).


Things I have tried/considered:



  • antialiasing reduces, but does not entirely eliminate, the seams

  • turning off mipmapping, has no effect


  • render each tile individually and extrude the edges by 1px - but this is a de-optimisation, since it can no longer render regions of tiles in one go, and creates other artefacts along the edges of areas of transparency

  • add a 1px border around source images and repeat the last pixels - but then they are no longer power-of-two, causing compatibility problems with systems without NPOT support

  • writing a custom shader to handle tiled images - but then what would you do differently? GL_REPEAT should be grabbing the pixel from the opposite side of the image at the borders, and not pick transparency.

  • the geometry is exactly adjacent, there are no floating point rounding errors.

  • if the fragment shader is hard coded to return the same color, the seams disappear.

  • if the textures are set to GL_CLAMP instead of GL_REPEAT, the seams disappear (although the rendering is wrong).

  • if the textures are set to GL_MIRRORED_REPEAT, the seams disappear (although the rendering is wrong again).

  • if I make the background red, the seams are still white. This suggests it's sampling opaque white from somewhere rather than transparency.


So the seams appear only when GL_REPEAT is set. For some reason in this mode only, at the edges of the geometry there is some bleed/leakage/transparency. How can that be? The entire texture is opaque.




Answer



The seams are correct behaviour for sampling with GL_REPEAT. Consider the following tile:


border tile


Sampling on the edges of the tile using fractional values mixes colors from the edge and the opposite edge, resulting in wrong colors: The left edge should be green, but is a mix from green and beige. The right edge should be beige, but is a mixed color as well. Especially the beige lines on the green background are very visible, but if you look closely you can see the green bleeding into the edge:


scaled tile



antialiasing reduces, but does not entirely eliminate, the seams



MSAA works by taking more samples around polygon edges. Samples taken left- or right of the edge will be "green" (again, considering the left edge of the first picture), and only one sample will be "on" the edge, partialy sampling from the "beige" area. When the samples are mixed the effect will be reduced.


Possible solutions:



In any case you should switch to GL_CLAMP to prevent bleeding from the pixel at the opposite side of the texture. Then you have three options:




  1. Enable anti-aliasing to smooth the transition between the tiles a bit.




  2. Set the texture filtering to GL_NEAREST. This gives all pixels hard edges, so polygon/sprite edges become indistinguishable, but it is obviously changes the style of the game quite a bit.




  3. Add the already discussed 1px border, just make sure the border has the color of the adjacent tile (and not the color of the opposite edge).




    • This might also be a good time to switch to a texture atlas (just make it bigger if you are concerned about NPOT support).

    • This is the only "perfect" solution, enabling GL_LINEAR-like filtering across the edges of polygons/sprites.




No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...