I'm trying to render a top-down tiled map using OpenGL, but occasionally I get an artefact like this where the background color (set to red so it is distinguishable) bleeds through.
This is not rendered as one texture, but as a grid of textures (each tile can be different, but in this example, they're all the same). No transparent parts in the tiles.
I've tried many things without success, including different values for GL_TEXTURE_WRAP_* and GL_TEXTURE_*_FILTER parameters.
The sprite sheet in use is this simple 32x32 image:
The top-left 16x16 tile is used as the only map tile.
Without glClear the artefact cannot be seen anymore, but I imagine it's still there, only not properly distinguishable in this situation since most of the image is blue and without clear, the previous image (still mostly blue) is what is bleeding through.
A simplified version of the code, with some supposedly irrelevant bits removed:
#include <glad/glad.h>
#include <SDL2/SDL.h>
#include <stdio.h>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
static GLuint texture;
static GLuint map_program;
static GLuint map_vbo;
static GLuint map_instance_vbo;
static GLuint map_vao;
const int MAP_WIDTH = 200;
const int MAP_HEIGHT = 100;
static GLuint load_shader_program(const char *vertex_shader_filename, const char *fragment_shader_filename) {
/* load, and link shaders */
}
static GLuint load_texture(const char *filename) {
GLuint tex;
int w, h, channels;
uint8_t *img;
GLenum err;
stbi_set_flip_vertically_on_load(1);
img = stbi_load(filename, &w, &h, &channels, 0);
if (img == NULL) {
printf("Unable to load image. stb_image error: %s\n",
stbi_failure_reason());
exit(1);
}
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA,
GL_UNSIGNED_BYTE, img);
stbi_image_free(img);
err = glGetError();
if (err != GL_NO_ERROR) {
printf("OpenGL error %d while loading image file: %s.\n",
(int) err, filename);
exit(1);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
return tex;
}
static void init_map(void)
{
map_program = load_shader_program("map-vertex-shader.glsl", "fragment-shader.glsl");
int vertex_data[] = {
0, 1, 3, /* triangle 1 */
1, 2, 3, /* triangle 2 */
};
int map_size = MAP_WIDTH * MAP_HEIGHT;
float *instance_data = malloc(map_size * 4 * sizeof(GLfloat));
float *base;
for (int y = 0; y < MAP_HEIGHT; ++y) {
for (int x = 0; x < MAP_WIDTH; ++x) {
/* bottom-left texture coordinates */
base = instance_data + 4*((MAP_WIDTH * y) + x);
base[0] = 0.0f;
base[1] = 0.5;
/* top-right texture-coordinates */
base[2] = 0.5f;
base[3] = 1.0f;
}
}
glGenVertexArrays(1, &map_vao);
glGenBuffers(1, &map_vbo);
glGenBuffers(1, &map_instance_vbo);
glBindVertexArray(map_vao);
glBindBuffer(GL_ARRAY_BUFFER, map_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_data), vertex_data, GL_STATIC_DRAW);
GLint index_attr = glGetAttribLocation(map_program, "index");
glVertexAttribIPointer(index_attr, 1, GL_INT, 1 * sizeof(int),
(void *) 0);
glEnableVertexAttribArray(index_attr);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, map_instance_vbo);
glBufferData(GL_ARRAY_BUFFER, map_size * 4 * sizeof(GLfloat), instance_data, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, map_instance_vbo);
GLint tex_coords_attr = glGetAttribLocation(map_program, "tile_texture_coords");
glEnableVertexAttribArray(tex_coords_attr);
glVertexAttribPointer(tex_coords_attr, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
glVertexAttribDivisor(tex_coords_attr, 1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
free(instance_data);
glUseProgram(map_program);
glUniform1i(glGetUniformLocation(map_program, "map_width"), MAP_WIDTH);
glUniform2f(glGetUniformLocation(map_program, "camera_pos"), 0, 1.34);
glUniform2f(glGetUniformLocation(map_program, "camera_size"), 119, 67);
glUseProgram(0);
}
static void render(void)
{
glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(map_program);
glBindVertexArray(map_vao);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6,
MAP_WIDTH * MAP_HEIGHT);
glBindVertexArray(0);
glUseProgram(0);
glBindVertexArray(0);
glUseProgram(0);
}
static void handle_events(SDL_Event *e, SDL_Window *window, int *quit) {
switch (e->type) {
case SDL_QUIT:
*quit = 1;
break;
case SDL_WINDOWEVENT:
if (e->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
int winw, winh;
SDL_GetWindowSize(window, &winw, &winh);
glViewport(0, 0, winw, winh);
}
break;
}
}
int main(int argc, char *argv[]) {
if (SDL_Init(SDL_INIT_EVERYTHING) < 0) {
printf("SDL could not be initialized. SDL_Error: %s\n",
SDL_GetError());
return 1;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
SDL_Window *window = SDL_CreateWindow("foo", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
-1, -1, SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
SDL_GL_CreateContext(window);
gladLoadGLLoader((GLADloadproc) SDL_GL_GetProcAddress);
texture = load_texture("sheet.png");
init_map();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
SDL_ShowWindow(window);
glBindTexture(GL_TEXTURE_2D, texture);
SDL_Event e;
int quit = 0;
while (!quit) {
if (SDL_PollEvent(&e))
handle_events(&e, window, &quit);
render();
SDL_GL_SwapWindow(window);
}
return 0;
}
The vertex shader used:
#version 330 core
// vertex attributes
in int index;
// instance attributes
in vec4 tile_texture_coords;
// output
out vec4 coords;
out vec2 texture_coords;
// uniforms
uniform int map_width;
uniform vec2 camera_pos;
uniform vec2 camera_size;
void main()
{
vec2 tile;
vec2 pos;
// One instance is run for each tile, so we get the tile
// position based on the instance ID.
tile = vec2(gl_InstanceID % map_width,
gl_InstanceID / map_width);
// "index" determines which tile vertex we have.
switch (index) {
case 0: // bottom-left
pos = tile;
texture_coords = tile_texture_coords.xy;
break;
case 1: // top-left
pos = tile + vec2(0, 1);
texture_coords = tile_texture_coords.xw;
break;
case 2: // top-right
pos = tile + vec2(1, 1);
texture_coords = tile_texture_coords.zw;
break;
case 3: // bottom-right
pos = tile + vec2(1, 0);
texture_coords = tile_texture_coords.zy;
break;
}
// camera transform
pos = 2 * (pos - camera_pos) / camera_size;
pos -= vec2(1, 1);
coords = vec4(pos, 0.0, 1.0);
gl_Position = coords;
}
And the fragment shader:
#version 330 core
in vec4 coords;
in vec2 texture_coords;
out vec4 frag_color;
uniform sampler2D texture0;
void main()
{
frag_color = texture(texture0, texture_coords);
}