13

When its translated in an integral value (1,2,3, etc....), there are no black lines in-between the tiles, it looks fine. But when it's translated to a non-integral (1.1, 1.5, 1.67), there are small blackish lines between each tile (I'm imagining that it's due to subpixel rendering, right?) ... and it doesn't look pretty =P

So... what should I do?

This is my image-loading code, by the way:

bool Image::load_opengl() {
    this->id = 0;

    glGenTextures(1, &this->id);

    this->bind();

    // Parameters... TODO: Should we change this?
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, this->size.x, this->size.y,
   0, GL_BGRA, GL_UNSIGNED_BYTE, (void*) FreeImage_GetBits(this->data));

    this->unbind();

    return true;
}

I've also tried using:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

and:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

Here is my image drawing code:

void Image::draw(Pos pos, CROP crop, SCALE scale) {
    if (!this->loaded || this->id == 0) {
        return;
    }

    // Start position & size
    Pos s_p;
    Pos s_s;

    // End size
    Pos e_s;

    if (crop.active) {
        s_p = crop.pos / this->size;
        s_s = crop.size / this->size;
        //debug("%f %f", s_s.x, s_s.y);
        s_s = s_s + s_p;
        s_s.clamp(1);
        //debug("%f %f", s_s.x, s_s.y);
    } else {
        s_s = 1;
    }

    if (scale.active) {
        e_s = scale.size;
    } else if (crop.active) {
        e_s = crop.size;
    } else {
        e_s = this->size;
    }

    // FIXME: Is this okay?
    s_p.y = 1 - s_p.y;
    s_s.y = 1 - s_s.y;

    // TODO: Make this use VAO/VBO's!!
    glPushMatrix();

        glTranslate(pos.x, pos.y, 0);

        this->bind();

        glBegin(GL_QUADS);

            glTexCoord2(s_p.x, s_p.y);
            glVertex2(0, 0);

            glTexCoord2(s_s.x, s_p.y);
            glVertex2(e_s.x, 0);

            glTexCoord2(s_s.x, s_s.y);
            glVertex2(e_s.x, e_s.y);

            glTexCoord2(s_p.x, s_s.y);
            glVertex2(0, e_s.y);

        glEnd();

        this->unbind();

    glPopMatrix();
}

OpenGL Initialization code:

void game__gl_init() {
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluOrtho2D(0.0, config.window.size.x, config.window.size.y, 0.0);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glEnable(GL_TEXTURE_2D);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}

Screenshots of the issue:

Screenshot 1 Screenshot 2

MiJyn
  • 4,117
  • 4
  • 34
  • 58
  • You are being a bit vague, could you clarify what you are doing? Are you drawing a set of adjacent tiles, and translating them by an amount? Or are you drawing repeating texture patterns? Maybe provide a screenshot if it's complicated. – Full Frontal Nudity Oct 26 '13 at 20:54
  • @FullFrontalNudity Right, I'm drawing a lot of tiles, all beside each other (the end of one is the start of another). It uses a spritesheet that contains all of the tiles. Each tile, when drawn, is translated to the correct position, then drawn at 0, 0. – MiJyn Oct 26 '13 at 21:00
  • @FullFrontalNudity There, I added the image drawing code to the post :) – MiJyn Oct 26 '13 at 21:01
  • Can you include a screenshot, please? I can tell you right now that this sounds an awful lot like an issue related to using linear texture filtering and sampling off-texel-center. I have answered similar questions in-depth: [(`Texture Coordinates Near 1 Behave Oddly`)](http://stackoverflow.com/questions/19323188/texture-coordinates-near-1-behave-oddly/19323954#19323954) and [(`Sprite Sheet Textures Picking Up Edges Of Adjacent Texture`)](http://gamedev.stackexchange.com/questions/61796/sprite-sheet-textures-picking-up-edges-of-adjacent-texture/61800#61800). – Andon M. Coleman Oct 26 '13 at 21:06
  • could this be polygon smoothing or anti aliasing of some sort? Do you have any strange states enabled? – Full Frontal Nudity Oct 26 '13 at 21:13
  • @FullFrontalNudity I don't think so, but I'll add my initialization code – MiJyn Oct 26 '13 at 21:16
  • @AndonM.Coleman added 2 demonstrating the issue – MiJyn Oct 26 '13 at 21:18
  • If you initialize your spritesheet to have a white background before filling it with tiles, do the lines between images become white? If so, see my answer. – Andon M. Coleman Oct 26 '13 at 21:20
  • @AndonM.Coleman what do you mean? there are only a few transparent sections inside the image, and the tiles shown are definitely not one of them... do you mean clearing the screen with a white background instead of a black one? – MiJyn Oct 26 '13 at 21:23
  • No, I literally mean if you make the unused portions of the spritesheet white, do the lines become white? It sounds like an issue with where the texels are being sampled from, and you are getting bleeding from adjacent tiles. Let me put it this way, try creating a spritesheet with only 1 sprite in it, replace everything else with white. Then for the one type of sprite on screen that actually has a valid image, does it have white edges instead of black? – Andon M. Coleman Oct 26 '13 at 21:26
  • @AndonM.Coleman Yes, you're right, it becomes white – MiJyn Oct 26 '13 at 21:44

3 Answers3

24

The problem with using texture atlases (sprite sheets) and adjacent texels leaking has to do with the way linear texture filtering works.

For any point in the texture that is not sampled exactly at the center of a texel, linear sampling will sample 4 adjacent texels and compute the value at the location you asked as the weighted (based on distance from the sample point) average of all 4 samples.

Here's a nice visualization of the problem:

image0

Since you cannot use something like GL_CLAMP_TO_EDGE in a texture atlas, you need to create border texels around the edge of each texture. These border texels will prevent neighboring samples from completely different textures in the atlas from altering the image through weighted interpolation explained above.

Note that when you use anisotropic filtering, you may need to increase the width of the border. This is because anisotropic filtering will increase the size of the sample neighborhood at extreme angles.


To illustrate what I mean by using a border around the edge of each texture, consider the various wrap modes available in OpenGL. Pay special attention to CLAMP TO EDGE.

image1

Despite there being a mode called "Clamp to Border", that is actually not what we are interested in. That mode lets you define a single color to use as a border around your texture for any texture coordinates that fall outside of the normalized [0.0-1.0] range.

What we want is to replicate the behavior of CLAMP_TO_EDGE, where any texture coordinate outside the proper range for the (sub-)texture receives the value of the last texel center in the direction it was out of bounds in. Since you have almost complete control over the texture coordinates in an atlas system, the only scenario in which (effective) texture coordinates might refer to a location outside of your texture are during the weighted average step of texture filtering.

We know that GL_LINEAR will sample the 4 nearest neighbors as seen in the diagram above, so we only need a 1-texel border. You may need a wider texel border if you use anisotropic filtering, because it increases the sample neighborhood size under certain conditions.

Here's an example of a texture that illustrates the border more clearly, though for your purposes you can make the border 1 texel or 2 texels wide.

image2

(NOTE: The border I am referring to is not the black around all four edges of the image, but the area where the checkerboard pattern stops repeating regularly)

In case you were wondering, here is why I keep bringing up anisotropic filtering. It changes the shape of the sample neighborhood based on angle and can cause more than 4 texels to be used for filtering:

image3

The larger the degree of anisotropy you use, the more likely you will have to deal with sample neighborhoods containing more than 4 texels. A 2 texel border should be adequate for most anisotropic filtering situations.


Last but not least, here is how a packed texture atlas would be built that would replicate GL_CLAMP_TO_EDGE behavior in the presence of a GL_LINEAR texture filter:

(Subtract 1 from X and Y in the black coordinates, I did not proof read the image before posting.)

Due to border storage, storing 4 256x256 textures in this atlas requires a texture with dimensions 516x516. The borders are color coded based on how you would fill them with texel data during atlas creation:

  • Red = Replace with texel directly below
  • Yellow = Replace with texel directly above
  • Green = Replace with texel directly to the left
  • Blue = Replace with texel directly to the right

Effectively in this packed example, each texture in the atlas uses a 258x258 region of the atlas, but you will generate texture coordinates that map to the visible 256x256 region. The bordering texels are only ever used when texture filtering is done at the edges of textures in the atlas, and the way they are designed mimics GL_CLAMP_TO_EDGE behavior.

In case you were wondering, you can implement other types of wrap modes using a similar approach -- GL_REPEAT can be implemented by exchanging the left/right and top/bottom border texels in the texture atlas and a little bit of clever texture coordinate math in a shader. That is a little more complicated, so do not worry about that for now. Since you're only dealing with sprite sheets limit yourself to GL_CLAMP_TO_EDGE :)

genpfault
  • 47,669
  • 9
  • 68
  • 119
Andon M. Coleman
  • 39,833
  • 2
  • 72
  • 98
  • Thanks for the awesome answer (you didn't need to copy it over from gamedev though =) ), but this seems like a lot of work ... I just decided to take the easy route, and split the spritesheet into single files, and use that (my spritesheet only has 2 tiles that I use anyways XD). – MiJyn Oct 27 '13 at 04:02
  • @MiJyn: It can be a lot of work, but if you use a tool specifically designed for building spritesheets many of them have this feature built-in. The actual implementation of the feature is not discussed in this much detail usually though ;) For instance, in [TexturePacker](http://www.codeandweb.com/texturepacker/documentation) it is referred to as **Shape Padding**. – Andon M. Coleman Oct 27 '13 at 04:24
  • Although, come to think of it the description is all wrong in the documentation. The last thing you want if your sprite is opaque edge-to-edge is a transparent border, because then you wind up with artifacts like the ones in your example... – Andon M. Coleman Oct 27 '13 at 04:39
2

I had the same problem, as illustrated in this picture:

problem

The idea is to shrink the images in the atlas by one pixel, and replace the pixels with the color adjacent to the 1px "border". Once this is done, adjust the UV offset to account for the 1px border. In other words, the actual texture coordinates would be (for top left corner to bottom right corner): start_x + 1, start_y + 1, end_x - 1, end_y -1

Before:

bad atlas

After:

good atlas

After applying, this is the result:

fixed

MiJyn
  • 4,117
  • 4
  • 34
  • 58
Oleg Skripnyak
  • 121
  • 1
  • 9
  • If the UV coordinates are mapped, accounting for the 1px margin, is there a reason to have the border duplicate the adjacent pixels? – MiJyn Jul 19 '15 at 02:38
  • Well, thanks for editing my answer and post images on the page. But see, then tiles look distorted [link](https://yadi.sk/i/it5hSx1XiXRid), normal - [link](https://yadi.sk/i/RPuF4DxAiXRro). I think this happens because original atlas image losts this border pixels, but actually it should be assembled by neigbours as it was origionally mentioned by Andon M. Coleman – Oleg Skripnyak Aug 18 '15 at 23:37
0

Another problem if you have transparent pixel in your texture:

When OpenGL use linear filter to scale your texture, it blend some pixels with transparent pixel, but in most of the cases, the transparent pixel color is white so the result blended pixel haven't the expected color. To fix this, a solution is to create pre-multiplied alpha. I've created a script for achieve this on Gimp:

(define (precompute-alpha img color)

  (define w (car (gimp-image-width img)))
  (define h (car (gimp-image-height img)))

  (define img-layer (car (gimp-image-get-active-layer img)))

  (define img-mask (car (gimp-layer-create-mask img-layer ADD-ALPHA-TRANSFER-MASK)))
  (gimp-layer-add-mask img-layer img-mask)
  (define alpha-layer (car (gimp-layer-new img w h RGBA-IMAGE "alpha" 100 NORMAL-MODE)))
  (gimp-image-insert-layer img alpha-layer 0 -1)
  (gimp-edit-copy img-mask)
  (define floating-sel (car (gimp-edit-paste alpha-layer TRUE)))
  (gimp-floating-sel-anchor floating-sel)

  (define bg-layer (car (gimp-layer-new img w h RGBA-IMAGE "bg" 100 NORMAL-MODE)))
  (gimp-image-insert-layer img bg-layer 0 2)
  (gimp-context-set-background color)
  (gimp-drawable-fill bg-layer BACKGROUND-FILL)

  (set! bg-layer (car (gimp-image-merge-down img img-layer 0)))
  (define bg-mask (car (gimp-layer-create-mask bg-layer ADD-WHITE-MASK)))
  (gimp-layer-add-mask bg-layer bg-mask)

  (gimp-edit-copy alpha-layer)
  (set! floating-sel (car (gimp-edit-paste bg-mask TRUE)))
  (gimp-floating-sel-anchor floating-sel)

  (gimp-image-remove-layer img alpha-layer)
)

(script-fu-register "precompute-alpha"
    "Precompute Alpha"
    "Automatically precompute alpha"
    "Thomas Arbona"
    "2017"
    "2017"
    "*"
    SF-IMAGE    "Image"         0
    SF-COLOR    "Alpha Color"   '(0, 0, 0)
)

(script-fu-menu-register "precompute-alpha" "<Image>/Alpha")

Just open your image in Gimp, open Alpha > Precompute Alpha and pick a color to pre-compute the alpha on your image with this color.

Thomas Arbona
  • 896
  • 5
  • 11