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:
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:
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
.
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.
(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:
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:
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
:)