1

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.

Background bleeding 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:

Sprite sheet

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);
}
Elektito
  • 3,863
  • 8
  • 42
  • 72
  • Can it be a precision issue ? – Ôrel Jun 24 '20 at 20:15
  • I guess it could be, just can't imagine how it can be fixed. I guess I can make the tiles to overlap a tiny bit, but then I might be creating other artefacts. – Elektito Jun 24 '20 at 20:25
  • My bet is on a off-by-one error in the calculation of the y coordinate of the tiles. – Yunnosch Jun 24 '20 at 20:27
  • This is just one pixel, while the y coordinate used in the shader is in units of tiles. An off by one error there should result in a large gap. – Elektito Jun 24 '20 at 20:45
  • 1
    OpenGL guarantees gapless render if the vertices are shared across triangles. Here you are rendering 2 independent rectangles without sharing vertices. That is the reason why there will always be a bleeding in the edges. Easiest way to solve this is by drawing all triangles in a single draw call and use common vertices. – codetiger Jun 25 '20 at 04:40
  • Interesting. Does that work if the vertex coordinates are exactly the same, or should they be the exact same data like with DrawElements? – Elektito Jun 25 '20 at 07:14
  • @codetiger Is that really a requirement? That would mean you can't have completely different texture coordinates on different tiles – user253751 Jun 25 '20 at 10:32
  • 1
    The binary data for gl_position needs to be the same. Please read this answer https://stackoverflow.com/a/39960932/409315 – codetiger Jun 25 '20 at 10:56
  • I'll see if I can create a solution based on that in the afternoon. FWIW, I've already found a solution that seems to work: in the vertex shader, I offset the texture coordinates by a tiny bit (0.001) towards the inside of the tile. Ugly, but seems to work for now. But like I said, maybe I can find a better solution based your suggestion. – Elektito Jun 25 '20 at 11:39
  • So I did a couple of tests: using renderdoc, I inspected the exact values of vertex shader outputs for the offending line. They were the same. Also, if I change texture coordinates to use the whole texture, this doesn't happen. Put together with the solution I mentioned in my previous comment, I'm reasonably sure that this is a problem with texture sampling. The texture is sampled from outside the range, and since those texels are transparent, the background is seen through them. Looks like using GL_NEAREST is enough so that this doesn't happen, but apparently not. – Elektito Jun 25 '20 at 14:04

0 Answers0