17
\$\begingroup\$

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.

\$\endgroup\$
5
  • \$\begingroup\$ You could try setting your texture sampler to clamp, or adjusting the border color, as I suspect it may be related to that. Also, GL_Repeat simply wraps the UV coordinates, so I am personally a bit confused when you say it can only repeat the entire texture, rather than a portion of it. \$\endgroup\$
    – Evan
    Commented May 3, 2014 at 13:37
  • 1
    \$\begingroup\$ Is it possible you're introducing cracks in your mesh somehow? You can test for it by setting the shader to just return a solid color rather than sampling a texture. If you still see cracks at that point, they're in the geometry. The fact that antialiasing reduces the seams suggests this might be the issue, as antialiasing shouldn't affect texture sampling. \$\endgroup\$ Commented May 3, 2014 at 16:35
  • \$\begingroup\$ You can implement repeating of individual tiles in a texture atlas. You do need to do some extra texture coordinate math and insert border texels around each tile though. This article explains a lot of this (though mostly with a focus on D3D9's annoying texture coordinate convention). Alternatively, if your implementation is new enough you can use array textures for your tile map; assuming each tile has the same dimensions this will work great and will not require any extra coordinate math. \$\endgroup\$ Commented May 4, 2014 at 2:34
  • \$\begingroup\$ 3D textures with GL_NEAREST sampling in the R coordinate direction also work just as well as array textures for most things in this scenario. Mipmapping is not going to work, but judging by your application you probably don't need mipmaps anyway. \$\endgroup\$ Commented May 4, 2014 at 2:38
  • 1
    \$\begingroup\$ In what way is the rendering "wrong", when set to CLAMP? \$\endgroup\$ Commented May 21, 2014 at 12:02

6 Answers 6

1
+100
\$\begingroup\$

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.
\$\endgroup\$
1
  • \$\begingroup\$ OOh, thanks - the wrapping around the texture does explain it. I'll look in to those solutions! \$\endgroup\$ Commented May 24, 2014 at 11:57
6
\$\begingroup\$

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.

Consider the display of an single, ordinary textured quad in OpenGL. Are there any seams, at any scale? No, never. Your goal is to get all your scene tiles packed tightly onto a single texture, then send that to the screen. EDIT To clarify this further: If you have discrete bounding vertices on each tile quad, you will have seemingly-unjustified seams under most circumstances. It's how GPU hardware works... floating point errors create these gaps based on the current perspective... errors which are avoided on a unified manifold, since if two faces are adjacent within the same manifold (submesh), the hardware will render them without seams, guaranteed. You've seen this in countless games and apps. You must have one tightly-packed texture on a single submesh without doubled vertices to avoid this once and for all. It's an issue that comes up time and again on this site and elsewhere: if you don't merge the corner vertices of your tiles (every 4 tiles share a corner vertex), expect seams.

(Consider that you may not even need vertices except at the four corners of the entire map... depends on your approach to shading.)

To solve: render (at 1:1) all of your tile textures into an FBO/RBO with no gaps, then send that FBO to the default framebuffer (the screen). Because the FBO itself is basically a single texture, you cannot end up with gaps on scaling. All texel boundaries that don't fall on a screen pixel boundary are going to be blended if you're using GL_LINEAR.... which is exactly what you want. This is the standard approach.

This also opens up a number of different routes to scaling:

  • scaling the size of the quad to which you will render the FBO
  • changing the UVs on that quad
  • fiddling with camera settings
\$\endgroup\$
8
  • \$\begingroup\$ I don't think this will work well for us. It sounds like you're proposing that at 10% zoom we render an area 10 times bigger. This does not bode well for devices with limited fill rate. Also I'm still mystified why specifically GL_REPEAT only causes seams, and no other mode does. How could that be? \$\endgroup\$ Commented May 22, 2014 at 23:55
  • \$\begingroup\$ @AshleysBrain Non sequitur. I'm mystified by your statement. Please explain how increasing zoom by 10% increases fill rate by 10x? Or how increasing zoom by 10x does -- since viewport clipping would prevent more than 100% fill rate on a single pass? I'm suggesting you display exactly what you're already intending to -- no more, no less -- in what is likely a more efficient AND seamless fashion, by unifying your final output using RTT, a common technique. Hardware will handle viewport clipping, scaling, blending and interpolation at virtually no cost, given this is only a 2D engine. \$\endgroup\$
    – Engineer
    Commented May 23, 2014 at 0:45
  • \$\begingroup\$ @AshleysBrain I have edited my question to make the issues with your current approach crystal clear. \$\endgroup\$
    – Engineer
    Commented May 23, 2014 at 1:07
  • \$\begingroup\$ @ArcaneEngineer Hi, I have tried your approach which solves perfectly the seams issue. However, now, instead of having seams I have pixel errors. You can have an idea of the issue I'm refering to here. Do you have any idea what might be the cause of this? Note that I'm doing everything in WebGL. \$\endgroup\$
    – Nemikolh
    Commented May 5, 2016 at 18:39
  • 1
    \$\begingroup\$ @ArcaneEngineer I'll ask a new question, explaining here is too cumbersome. I'm sorry for the annoyance. \$\endgroup\$
    – Nemikolh
    Commented May 5, 2016 at 19:50
1
\$\begingroup\$

If the images should appear as one surface, render these as one surface texture (scale 1x) on one 3D mesh/plane. If you render each as a separate 3D object it will always have seams due to rounding errors.

The answer by @Nick-Wiggill is correct, i think you misunderstood it.

\$\endgroup\$
0
\$\begingroup\$

2 possibilities that might be worth trying:

  1. Use GL_NEAREST instead of GL_LINEAR for your texture filtering in. (As pointed out by Andon M. Coleman)

    GL_LINEAR will (in effect) blur the image ever so slightly (interpolating between the nearest pixels), which allows for a smooth transition in color from one pixel to the next. This can help make textures look better in many cases, since it prevents your textures from having those blocky pixels all over it. It also makes it so that a little bit of brown from one pixel of one tile over can get blurred over to the adjacent green pixel of the adjacent tile.

    GL_NEAREST just finds the nearest pixel and uses that color. No interpolation. As a result, it's actually a little faster to.

  2. Shrink the texture coordinates for each tile a little.

    Sort of like adding that extra pixel around each tile as a buffer, except instead of expanding the tile on the image, you shrink the tile in the software. 14x14px tiles while rendering instead of 16x16px tiles.

    You might even be able to get away with 14.5x14.5 tiles without too much brown being mixed in.

--edit--

Visualization of the explanation in option #2

As shown in the image above, you can still easily use a power of two texture. So you can support hardware that doesn't support NPOT textures. What changes is your texture coordinates, instead of going from (0.0f, 0.0f) to (1.0f, 1.0f) You'd go from (0.03125f, 0.03125f) to (0.96875f, 0.96875f).

That puts your tex-coords slightly inside the tile which reduces the effective resolution in game (though not on hardware so that you still have power-of-two textures), however should have the same rendering effect as expanding the texture.

\$\endgroup\$
2
  • \$\begingroup\$ I already mentioned I tried GL_NEAREST (aka point filtering), and it doesn't fix it. I can't do #2 because I need to support NPOT devices. \$\endgroup\$ Commented May 20, 2014 at 17:18
  • \$\begingroup\$ Made an edit that might clear up some things. (I'm assuming that "need to support NPOT [(non-power-of-two)] devices" was meant to mean something along the lines of you need to keep the textures at some power of 2?) \$\endgroup\$ Commented May 20, 2014 at 19:07
0
\$\begingroup\$

Here is what I think that might be happening: Where two tiles collide, the x component of their edges should be equal. If that is not true, they might get rounded into the opposite direction. So make sure that they have exactly the same x value. How do you ensure that this happens? You might try to, lets say, multiply by 10, round and then divide by 10 (only do this on the CPU, before your vertices are going into the vertex shader). That should give correct results. Also, do not draw tile by tile using transformation matrices, but put them in a batch VBO to make sure that the wrong results are not coming from the lossy IEEE floating point system in combination with matrix multiplications.

Why do I suggest this? Because from my experience, if the vertices have the exact same coordinates when they come out of the vertex shader, filling the corresponding triangles will create seamless results. Keep in mind that something that is mathematically correct, might be a bit off due to IEEE. The more calculations you perform on your numbers, the less accurate the result will be. And yes, matrix multiplications require quite some operations, that might be done in only a multiplication and an addition when creating your VBO, which will give more accurate results.

What also might be the problem is that you are using a spritesheet (also called atlas) and that when sampling the texture, the pixels of the adjacent tile texture gets picked. To ensure that this doesn't happen is to create a tiny border in the UV mappings. So, if you have a 64x64 tile, your UV mapping should cover a bit less. How much? I think I used in my games 1 quarter of a pixel at each side of the rectangle. So offset your UV's by 1/(4*widthOfTheAtlas) for x components and 1/(4*heightOfTheAtlas) for y components.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks, but as I already noted the geometry is definitely exactly adjacent - in fact all the co-ordinates result in exact integers, so it's easy to tell. No atlases are used here. \$\endgroup\$ Commented May 21, 2014 at 10:18
0
\$\begingroup\$

To solve this in 3D space, I mixed texture arrays with Wolfgang Skyler's suggestion. I put a 8-pixel border around my real texture for each tile (120x120, 128x128 total) which, for each side, which I could fill with "wrapped" image or the side just being extended. The sampler reads this area when it is interpolating the image.

Now with filtering and mipmapping, the sampler can still easily read past the entire 8 pixel border. To catch that small problem (I say small because it only happens on a few pixels when geometry is really skewed or far away), I split the tiles into a texture array, so each tile has its own texture space and can use clamping on the edges.

In your case (2D/flat), I would certainly go with rendering a pixel perfect scene, and then scaling the desired portion of the result into the viewport, as suggested by Nick Wiggil.

\$\endgroup\$

You must log in to answer this question.

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