# Attempt to fix sprite sheet pixel bleeding in OpenGL 2D causing sprite distortion

While working on a project, I encountered the common problem of pixel bleeding when trying to draw subregions of my sprite sheet. This caused sort of "seams" to appear at the edges of my sprites. You can see the issue here, on the right and top of the sprite .

Doing some searching, I found others with a similar problem, and a suggested solution (here, and here for example) was to offset my texture coordinates by a bit, such as 0.5. I tried this, and it seemed to work. But I have noticed that sometimes, depending on where the sprite or camera is, I get a bit of distortion on the sprites. Here, the left side appears to be cut off, and here, the bottom seems to have expanded. (I should note, the distortion happens on all sides, I just happened to take screenshots of it happening on the bottom and left.) It may be a little difficult to see in screenshots, but it is definitely noticeable in motion. For reference, here is the part of the sprite sheet that is being displayed here

Does anybody have any idea what is going on here? I didn't actually notice this issue until recently. I originally set out to resolve the pixel bleeding when I saw it occurring between my tile sprites. This new issue does not occur with them using my current half-pixel offset solution (or if it does, it's not noticeable).

Code:

Texture parameters

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);


Texture coordinate calculation

std::vector<glm::vec4> Texture2D::GetUVs(int w, int h)
{
std::vector<glm::vec4> uvs;
int rows = Width/ w;
int columns = Height / h;

for(int c = 0; c < columns; c ++)
{
for(int i = 0; i < rows; i ++)
{
float offset = 0.5;
uvs.emplace_back(glm::vec4(float(((i) * w + offset))/Width,
float(((1 + i) * w - offset))/Width,
float(((c) * h + offset))/Height,
float(((1 + c) * h - offset))/Height));
}
}
return uvs;


Where Width and Height are the dimensions of the sprite sheet, and w and h are the dimensions of the subregion, in this case 32 and 32.

How I pass the uvs to the shader

GLfloat verticies[] =
{
uv.x, uv.w,
uv.y, uv.z,
uv.x, uv.z,

uv.x, uv.w,
uv.y, uv.w,
uv.y, uv.z
};



Where uv is the uv at an index in the uvs vector that was returned above in the GetUVs function.

#version 330 core
layout (location = 0) in vec2 vertex;

out vec2 TextureCoordinates;

uniform vec2 uvs[6];
uniform mat4 model;
uniform mat4 projection;

void main()
{
const vec2 position [6] = vec2[]
(
vec2(0.0f, 1.0f),
vec2(1.0f, 0.0f),
vec2(0.0f, 0.0f),

vec2(0.0f, 1.0f),
vec2(1.0f, 1.0f),
vec2(1.0f, 0.0f)
);

TextureCoordinates = uvs[gl_VertexID];
gl_Position = projection * model * vec4(position[gl_VertexID], 0.0, 1.0);
}


#version 330 core
in vec2 TextureCoordinates;
out vec4 color;

uniform sampler2D image;
uniform vec4 spriteColor;

void main()
{
color = vec4(spriteColor) * texture(image, TextureCoordinates);
}


Thanks for reading. I have asked this question a few places and not gotten any response, so any help is greatly appreciated.

• I would remove offsets as it is an obvious "hack" (cutting 0.5 from each side you "steal" the whole pixel off of the sprite) and added clear regions padding around the sprites in the atlas (1-2px transparent borders). – Kromster Nov 6 '20 at 6:07
• Thanks for the response. I had seen a similar solution suggested while looking for a solution to the initial problem. It was suggested to use texture packing, something like in this video youtube.com/watch?v=MVsIIkJNkjM Would this still be the solution for animated sprites, or would the clear border be good enough? It seems like it would be a little tricky to do this for non-tiles sprites. – starrider Nov 6 '20 at 7:32
• If all of your sprites are the same size you could use an array texture rather than a spritesheet, which would allow for GL clamp/wrap modes, mipmapping, linear filtering without bleeding or padding, and 0..1 texture coords; while also meeting the objective of only requiring a single texture bind. – Maximus Minimus Nov 6 '20 at 8:14
• Thanks for the response. On the full sprite sheet there is in fact some variation on sprite size, so I don't know if that would work. Even if it would, I have quite a few sprites on this sheet, and I worry that if they were all split up into individual images, things would get a little hard to manage. – starrider Nov 6 '20 at 9:19

In case anyone ever has a similar problem and finds this, I was able to find a very simple solution. The issue occurs when attempting to render at non-whole numbers, so the solution is to simply insure that that never happens. Right before I render, I round the position of the sprites and my camera to whole numbers. An example of my render function:

void DrawSprite(Texture2D &texture, glm::vec2 position, float rotate, glm::vec2 size)
{
glm::mat4 model;
position.x = round(position.x);
position.y = round(position.y);
model = glm::translate(model, glm::vec3(position, 0.0f));

//the rest of the model transforms
//pass the model transform to the shader
}


There is still potentially an issue of either bleed or deforming when rotating sprites, but that is a separate problem.