0

I just started learning opengl technology. My program draw 2d isometric tiles and program output this: enter image description hereBe unknown reasons black lines appear when two textures overlap or two textures touch.

Code example:

typedef unsigned int ID;

class GraphicEngine {
public:
    GraphicEngine();
    ~GraphicEngine();
    void initShaders(const char* vertexShaderSource, const char* fragmentShaderSource);
    void initRenderData(float vertices[], unsigned int size);
    std::vector<ID> initTextures(std::vector<std::string>& paths);
    void drawTextures(std::vector<ID> testuresIds);

private:
    GraphicEngine(GraphicEngine&) = delete;
    GraphicEngine(GraphicEngine&&) = delete;
    GraphicEngine& operator=(const GraphicEngine& other) = delete;
private:
    unsigned int VBO = 0; 
    unsigned int VAO = 0;
    unsigned int EBO = 0;
    unsigned int shaderProgram;
};

GraphicEngine::GraphicEngine() {

}

GraphicEngine::~GraphicEngine() {
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
}


void GraphicEngine::initShaders(const char* vertexShaderSource, const char* fragmentShaderSource) {
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    shaderProgram = glCreateProgram();
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
}

void GraphicEngine::initRenderData(float vertices[], unsigned int size) {
    unsigned int indices[] = {
        0, 1, 3,
        1, 2, 3
    };
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
}

std::vector<ID> GraphicEngine::initTextures(std::vector<std::string>& paths) {
    std::vector<ID> ids(paths.size());
    stbi_set_flip_vertically_on_load(true);
    for (int i = 0; i < paths.size(); i++) {
        unsigned int texture;
        glGenTextures(1, &ids[i]);
        glBindTexture(GL_TEXTURE_2D, ids[i]);
        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_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        int width, height, nrChannels;
        unsigned char* data = stbi_load(paths[i].c_str(), &width, &height, &nrChannels, STBI_rgb_alpha);
        if (data)
        {
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
            glGenerateMipmap(GL_TEXTURE_2D);
        }
        stbi_image_free(data);
    }
    return ids;
}

void GraphicEngine::drawTextures(std::vector<ID> testuresIds) {
    static bool ex = false;
    for (auto testureId : testuresIds) {
        for (int i = 0; i < 4; i++) {
            glBindTexture(GL_TEXTURE_2D, testureId);
            glm::mat4 transform = glm::mat4(1.0f);
            transform = glm::translate(transform, glm::vec3(i * 0.6f + 0.0f, 0.0f, 0.0f));
            glUseProgram(shaderProgram);
            unsigned int transformLoc = glGetUniformLocation(shaderProgram, "transform");
            glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        }
        for (int i = 0; i < 4; i++) {
            glBindTexture(GL_TEXTURE_2D, testureId);
            glm::mat4 transform = glm::mat4(1.0f);
            transform = glm::translate(transform, glm::vec3(i * 0.6f - 0.3f, -0.16f, 0.0f));
            glUseProgram(shaderProgram);
            unsigned int transformLoc = glGetUniformLocation(shaderProgram, "transform");
            glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        }
    }

const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

Window::Window():window(nullptr) {}

Window::~Window() {
    glfwTerminate();
}

bool Window::initWindowResources() {
    bool result = false;
    if (glfwInit() == GLFW_TRUE) {
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
        if (window != nullptr) {
            glfwMakeContextCurrent(window);
            if (glfwSetFramebufferSizeCallback(window, [](GLFWwindow* window, int width, int height) {
                glViewport(0, 0, width, height); }) == NULL) {
                if (gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
                    result = true;
                }
            }
        }
    }
    return result;
}

const char* vertexShaderSource =
"#version 330 core\n"
"layout(location = 0) in vec3 aPos;\n"
"layout(location = 1) in vec2 aTexCoord;\n"
"out vec2 TexCoord;\n"
"uniform mat4 transform;\n"
"void main()\n"
"{\n"
"    gl_Position = transform * vec4(aPos, 1.0);\n"
"    TexCoord = vec2(aTexCoord.x, aTexCoord.y);\n"
"}\n\0";

const char* fragmentShaderSource =
"#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"in vec2 TexCoord;\n"
"uniform sampler2D texture1;\n"
"void main()\n"
"{\n"
"    FragColor = texture(texture1, TexCoord);\n"
"}\n\0";

void Window::mainWindowLoop() {

    graphicEngine.initShaders(vertexShaderSource, fragmentShaderSource);
    std::vector<std::string> pathsTextures = { "C:\\Users\\Олег\\\Desktop\\sea1.png" };
    float vertices[] = {
        // positions          // colors           // texture coords
        -1.3f,  0.16f, 0.0f,  1.0f, 1.0f, // top right
        -1.3f, -0.16f, 0.0f,  1.0f, 0.0f, // bottom right
        -0.7f, -0.16f, 0.0f,  0.0f, 0.0f, // bottom left
        -0.7f,  0.16f, 0.0f,  0.0f, 1.0f  // top left 
    };
    graphicEngine.initRenderData(vertices, sizeof(vertices));
    std::vector<ID> idsTextures = graphicEngine.initTextures(pathsTextures);
    while (!glfwWindowShouldClose(window))
    {
        glClearColor(1.0f, 1.0f, 1.0f, 1.0f);

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        graphicEngine.drawTextures(idsTextures);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
}

int main()
{
    Window window;
    if (window.initWindowResources()) {
        window.mainWindowLoop();
    }

    return 0;
}

Png: Size png: 62x34 pixels, Transparent sprite, use prog to created png: piskelapp

Please, pvoide information about this issue: inforamtion about reasons of this issue and how to fix this issue.

2 Answers2

0

I was able to reproduce your issue. You are working with non-premultiplied alpha, this is known for producing undesirable results when rendering translucent images. Take a look at this article: http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/

Now, to solve your problem, first change your blend function to glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA). Second, stbi doesn't pre-multiply the alpha on load, you have to do it manually. Each pixel is composed by 4 bytes, red, green, blue and alpha, on the 0-255 range. Convert each value to the normalized range (0.0f - 1.0f) by dividing by 255.0f, multiply r, g, and b by alpha, then multiply it back by 255.0f;

Space.cpp
  • 31
  • 3
0

The dark lines at the edge of the tiles are results of alpha blending and texture filtering.

The linked tile image (PNG) contains three premultipled color channels (red, green, blue) and transparency information (alpha channel) with no partially transparent pixels (the alpha value is either 1.0 or 0.0 everywhere, which results in sharp edges):

tile image from original post

channels of original tile image

This can be checked in an image editor (for example Gimp). The image uses premultiplied alpha, i.e. the color channels were masked by the alpha channel and only contain color information where the alpha channel is non-zero.

The area outside of the valid image region is all black, so when OpenGL uses linear texture interpolation (GL_LINEAR) it will mix the hidden black texels right at the edge with the visible colored texels, which can result in a dark color, depending on the used blending function.

Alpha blending mixes the already present color in the framebuffer (of the cleared background or the already written fragments) with the incoming ones.

The used blending function glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) instructs the hardware to do this for every pixel:

GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA illustrated

The result: dark artifacts at the edges of each tile caused by the interpolated alpha value at the edges of the tile, which darkens the source color (sRGB * sA) (modified example with original tile image, reproduced issue from the original post):

modified example program using the original tile image, with glitchy blending

In other words:

https://shawnhargreaves.com/blog/texture-filtering-alpha-cutouts.html:

Texture filtering: alpha cutouts

(...)

Filtering applies equally to the RGB and alpha channels. When used on the alpha channel of a cutout texture it will produce new fractional alpha values around the edge of the shape, which makes things look nice and antialiased. But filtering also produces new RGB colors, part way in between the RGB of the solid and transparent parts of the texture. Thus the RGB values of supposedly transparent pixels can bleed into our final image.

This most often results in dark borders around alpha cutouts, since the RGB of transparent pixels is often black. Depending on the texture, the bleeding could alternatively be white, pink, etc.

To quick-fix the problem, the blending function could simply by changed to glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA), since the tile image already has premultiplied RGB channels, which are all black (= 0) in transparent areas:

GL_ONE, GL_ONE_MINUS_SRC_ALPHA illustrated

https://shawnhargreaves.com/blog/premultiplied-alpha.html:

Premultiplied alpha is better than conventional blending for several reasons:

  • It works properly when filtering alpha cutouts (...)
  • It works properly when doing image composition (...)
  • It is a superset of both conventional and additive blending. If you set alpha to zero while RGB is non zero, you get an additive blend. This can be handy for particle systems that want to smoothly transition from additive glowing sparks to dark pieces of soot as the particles age.

The result: dark artifacts disappear almost entirely after changing the blending function (modified example with original tile image, issue partially fixed):

modified example program using the original tile image, with improved blending

Not perfect.

To fix this, some pixels could be drawn around the tile to enlarge the visible area a bit:

enlarged tile

To let tiles overlap a bit, like that:

overlapped tiles

The result (with texture filtering, and overlapped pixels): overlapped tiles, linear texture filtering

(Additionally, lines/other graphical elements could be drawn on top of the artifacts to cover them up. And if the pixelated jagged edges are not wanted, the actual textured polygons quads could be replaced by rhombuses that could be placed precisely next to each other in a continuous mesh that could be rendered in one draw call, no alpha blending required anymore, however sharp edges do not fit a pixelated look I guess.)


A possible solution using GL_NEAREST:

  • OpenGL texture parameters:

    To get rid of the artefacts and blurred/filtered look, GL_LINEAR can be replaced by GL_NEAREST, which disables texture interpolation altogether for the selected texture and lets OpenGL render the raw pixels without applying texture filtering (GL_CLAMP_TO_EDGE makes sense here to avoid artifacts at the edges):

    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);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  • Power of Two Textures:

    OpenGL performance can be improved by always using texture dimensions that are a power of two, e.g. 64x32 (instead of 60x32 in your case). - The tile image could be modified, e.g.: 2 pixels added on each side (and borders marked): sea2.png

    Side note: This restriction is not that important anymore, but in the past it was even necessary to use a special extension to enable NPOT textures:

Conventional OpenGL texturing is limited to images with power-of-two dimensions and an optional 1-texel border. ARB_texture_non_power_of_two extension relaxes the size restrictions for the 1D, 2D, cube map, and 3D texture targets.

  • Snap to pixel:

    There are multiple ways to do this with OpenGL. I would recommend to scale the orthographic projection, so that 1 OpenGL coordinate unit exactly matches 1 texel unit. That way, tiles can be precisely placed on the pixel grid (just shift coordinates of the tile vertices by 64 pixels/OpenGL units left/right, to get to the next one, in this example). Coordinates could be represented as integers in the engine now.

Modified code example:

void GraphicEngine::drawTextures(std::vector<ID> testuresIds, float wndRatio) {
  const int countx = 3, county = 3; /* number of tiles */
  const float scale = 100.0f; /* zoom */
  const glm::mat4 mvp = glm::ortho(-wndRatio * scale, wndRatio * scale, -scale, scale, 2.0f, -2.0f);
  const float offx = -((countx * TILE_WIDTH * 2.0f) * 0.5f - TILE_WIDTH);
  const float offy = -TILE_WIDTH * 0.5f;
  for (auto testureId : testuresIds) {
    for (int y = 0; y < county; y++) {
      for (int x = 0; x < countx - (y & 1 ? 1 : 0); x++) {
        const glm::mat4 transform = mvp * glm::translate(glm::mat4(1.0f), glm::vec3(
          offx + x * TILE_WIDTH * 2.0f + (y & 1 ? TILE_WIDTH : 0.0f),
          offy + y * TILE_HEIGHT, 0.0f));
        glBindTexture(GL_TEXTURE_2D, testureId);
        const GLint transformLoc = glGetUniformLocation(shaderProgram, "transform");
        glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(transform));
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
      }
    }
  }
}

Screenshot of modified example: screenshot of modified example

And without marked edges: screenshot of modified example without marked edges


Some hints on the use of "straight alpha textures":

Another approach to solve this might be the use of an unmasked/unpremultiplied/straight alpha texture. The color channels of the original tile image can be flood filled out like this:

modified tile image

(Note: The linked PNG image above can't be used directly. Imgur seems to convert transparent PNG images and automatically masks the color channels...)

channels of modified tile image

This technique could help to reduce the artifacts when texture filtering and the conventional alpha blending function is used (i.e. GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). However, the background will always show through a tiny bit, because some pixels are always sightly transparent at the edges (caused by texture filtering):

modified example with linear texture interpolation

(The result should be very similar to the first solution above, where the original premultiplied image is used with modified alpha blending (GL_ONE, GL_ONE_MINUS_SRC_ALPHA).)

If the tile contains not just a plain color, the color information at the edges of the tile would need to be extended outwards to avoid artifacts.

Obviously this doesn't solve the original issue completely, when a precise 2D look is the goal. But it could be useful in other situations, where the hidden pixels also generate bad results when other forms of transparency/blending/compositing are used, e.g. for particles, semi-transparent edges of foliage, text etc.


Some general hints that could help to solve the issue:

GL_POLYGON_SMOOTH_HINT Indicates the sampling quality of antialiased polygons. Hinting GL_NICEST can result in more pixel fragments being generated during rasterization, if a larger filter function is applied.


The code in the original question comes with some issues, it does not compile like that (some parts of the class definitions are missing etc.). It takes some effort to reproduce to issue, which makes it more complicated to answer the question. - And it wasn't completely clear to me whether the intention is to just render seamless, pixelated tiles (solution: use GL_NEAREST), or if texture filtering is required...

Here my modified code example.

Related questions on Stack Overflow:

Some links related to Alpha Blending / "premultiplied alpha":

rel
  • 764
  • 5
  • 18