c++linuxopengl

Opengl stencil buffer bleeding


I have an opengl problem but the minimal code for showcase became 450 lines of code so I uploaded it to github instead:

https://github.com/Siamaster/opengltest

What I'm trying to do here is to define a path with nodes on the window and use the area in the stencil buffer and then draw a square where only the parts that are within the path is going to appear.

The program does the clipping successfully, only at startup. As I move the nodes around, the square will render outside the path. I log the node positions every 3 seconds for that sake,if I restart the application with starting node positions of last broken run the square is cut correctly. I can't figure out what fails to update.

You need cmake, opengl, glfw and glew to run this program on linux.

Here's the most interesting part of the code where I build the mesh and render it in a run loop:

void buildMesh(bool print = false) {
    if (polygonPoints.size() < 3) {
        std::cerr << "Not enough points for polygon\n";
        return;
    }

    std::vector<std::vector<Point>> polygon = { polygonPoints };
    indices = mapbox::earcut<uint32_t>(polygon);
    if (indices.size() % 3 != 0 || indices.empty()) {
        std::cerr << "Invalid triangulation, indices size: " << indices.size() << "\n";
        return;
    }

    if (print) {
        std::cout << "polygonPoints = {\n";
        for (const auto& pt : polygonPoints) {
            std::cout << "    {" << pt.x << ", " << pt.y << "},\n";
        }
        std::cout << "};\n";
        std::cout << "Indices size: " << indices.size() << "\nIndices: ";
        for (uint32_t index : indices) std::cout << index << " ";
        std::cout << "\n";
    }

    std::vector<float> vertexData;
    for (const auto& p : polygonPoints) {
        vertexData.push_back(p.x);
        vertexData.push_back(p.y);
    }

    // Update polygon mesh
    glBindVertexArray(stencilVAO);
    glBindBuffer(GL_ARRAY_BUFFER, stencilVBO);
    glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, stencilEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_DYNAMIC_DRAW);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    std::vector<float> lineVertexData;
    for (const auto& p : polygonPoints) {
        lineVertexData.push_back(p.x);
        lineVertexData.push_back(p.y);
    }

    glBindVertexArray(lineVao);
    glBindBuffer(GL_ARRAY_BUFFER, lineVbo);
    glBufferData(GL_ARRAY_BUFFER, lineVertexData.size() * sizeof(float), lineVertexData.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // Update circle mesh for vertices
    std::vector<float> circleVertexData;
    for (const auto& p : polygonPoints) {
        for (int i = 0; i < CIRCLE_SEGMENTS; ++i) {
            float theta = 2.0f * 3.1415926f * float(i) / float(CIRCLE_SEGMENTS);
            float x = p.x + NODE_RADIUS * cosf(theta);
            float y = p.y + NODE_RADIUS * sinf(theta);
            circleVertexData.push_back(x);
            circleVertexData.push_back(y);
        }
    }

    glBindVertexArray(circleVao);
    glBindBuffer(GL_ARRAY_BUFFER, circleVbo);
    glBufferData(GL_ARRAY_BUFFER, circleVertexData.size() * sizeof(float), circleVertexData.data(), GL_DYNAMIC_DRAW);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

int main() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_STENCIL_BITS, 8);

    window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Stencil test", nullptr, nullptr);

    glfwMakeContextCurrent(window);
    glewExperimental = GL_TRUE;
    glewInit() != GLEW_OK;


    glEnable(GL_STENCIL_TEST);

    stencilShader = createShaderProgram(stencilVertexShaderSource, stencilFragmentShaderSource);
    boxShader = createShaderProgram(boxVertexShaderSource, boxFragmentShaderSource);
    lineShader = createShaderProgram(lineVertexShaderSource, lineFragmentShaderSource);


    // Indices size: 9
    // Indices: 4 0 1 2 3 4 4 1 2

    // Polygon points (example)
    polygonPoints = {
        {796, 778},
        {1300, 500},
        {1300, 1300},
        {985, 893},
        {1275, 712},
    };

    glfwSetMouseButtonCallback(window, [](GLFWwindow* wnd, int button, int action, int mods) {
        if (button == GLFW_MOUSE_BUTTON_LEFT) {
            double mx, my;
            glfwGetCursorPos(wnd, &mx, &my);

            if (action == GLFW_PRESS) {
                for (int i = 0; i < (int)polygonPoints.size(); ++i) {
                    if (glm::distance(glm::vec2(mx, my), polygonPoints[i]) < NODE_RADIUS) {
                        selectedNode = i;
                        break;
                    }
                }
            }
            else if (action == GLFW_RELEASE) {
                selectedNode = -1;
            }
        }
    });

    glfwSetCursorPosCallback(window, [](GLFWwindow* wnd, double xpos, double ypos) {
        if (selectedNode != -1) {
            polygonPoints[selectedNode] = { xpos, ypos };

            buildMesh();
        }
    });

    float vertices[] = {
        0.0f, 0.0f,
        1.0f, 0.0f,
        1.0f, 1.0f,
        0.0f, 1.0f,
    };

    unsigned int sIndices[] = {
        0, 1, 2,
        2, 3, 0
    };

    glGenVertexArrays(1, &boxVAO);
    glGenBuffers(1, &boxVBO);
    glGenBuffers(1, &boxEBO);

    glBindVertexArray(boxVAO);
    glBindBuffer(GL_ARRAY_BUFFER, boxVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, boxEBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(sIndices), sIndices, GL_STATIC_DRAW);

    // Position attribute
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    double lastBuildTime = 0.0;
    bool showImage = true;

    glGenVertexArrays(1, &stencilVAO);
    glGenBuffers(1, &stencilVBO);
    glGenBuffers(1, &stencilEBO);
    glGenVertexArrays(1, &lineVao);
    glGenBuffers(1, &lineVbo);
    glGenVertexArrays(1, &circleVao);
    glGenBuffers(1, &circleVbo);

    buildMesh();

    glEnable(GL_STENCIL_TEST);

    while (!glfwWindowShouldClose(window)) {
        glfwGetFramebufferSize(window, &WINDOW_WIDTH, &WINDOW_HEIGHT);
        glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);


        double currentTime = glfwGetTime();
        double elapsed = currentTime - lastBuildTime;

        if (elapsed >= 3.0 + 1.0) {
            lastBuildTime = currentTime;
            buildMesh(true);
            showImage = true;
        } else if (elapsed >= 3.0) {
            // Hide image for 1 second
            showImage = false;
        } else {
            showImage = true;
        }


        const glm::mat4 projection = glm::ortho(0.0f, (float) WINDOW_WIDTH, (float) WINDOW_HEIGHT, 0.0f);

        glClearStencil(0);
        glClearColor(0, 0, 0, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

        glStencilFunc(GL_ALWAYS, 1, 0xFF);
        glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
        glStencilMask(0xFF);
        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); // No color output
        glUseProgram(stencilShader);
        glUniformMatrix4fv(glGetUniformLocation(stencilShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        glBindVertexArray(stencilVAO);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, stencilEBO);
        glDrawElements(GL_TRIANGLES, (GLsizei) indices.size(), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        if (showImage) {

            glm::mat4 model = glm::mat4(1.0f);
            model = glm::translate(model, glm::vec3(polygonPoints[0], 0.0f));
            model = glm::scale(model, glm::vec3(900 * 0.5f, 900 * 0.5f, 1.0f));
            glUseProgram(boxShader);
            glUniformMatrix4fv(glGetUniformLocation(boxShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
            glUniformMatrix4fv(glGetUniformLocation(boxShader, "model"), 1, GL_FALSE, glm::value_ptr(model));

            glStencilFunc(GL_EQUAL, 1, 0xFF);
            glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
            glStencilMask(0x00); // Don't write to stencil
            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); // Enable color output
            glBindVertexArray(boxVAO);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, boxEBO);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
            glBindVertexArray(0);

        } else {
            glStencilFunc(GL_EQUAL, 1, 0xFF); // Only draw where stencil == 1
            glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
            glStencilMask(0x00); // Don't modify stencil
            glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

            glUseProgram(lineShader); // Or any simple shader
            glUniformMatrix4fv(glGetUniformLocation(lineShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
            glUniform3f(glGetUniformLocation(lineShader, "color"), 0.2f, 0.2f, 0.8f); // e.g., blue overlay

            glBindVertexArray(stencilVAO); // Same VAO as the polygon mesh
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, stencilEBO);
            glDrawElements(GL_TRIANGLES, (GLsizei) indices.size(), GL_UNSIGNED_INT, 0);
            glBindVertexArray(0);
        }

        // Draw UI (outline and vertices) without stencil
        glStencilFunc(GL_ALWAYS, 0, 0xFF);
        glStencilMask(0x00);
        glUseProgram(lineShader);
        glUniformMatrix4fv(glGetUniformLocation(lineShader, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
        glUniform3f(glGetUniformLocation(lineShader, "color"), 0.0f, 1.0f, 0.0f);
        glBindVertexArray(lineVao);
        glDrawArrays(GL_LINE_LOOP, 0, (GLsizei) polygonPoints.size());
        glBindVertexArray(0);
        glBindVertexArray(circleVao);
        for (size_t i = 0; i < polygonPoints.size(); ++i) {
            glDrawArrays(GL_LINE_LOOP, i * CIRCLE_SEGMENTS, CIRCLE_SEGMENTS);
        }
        glBindVertexArray(0);

        glfwSwapBuffers(window);
        glfwPoll

Events(); }


Solution

  • The bug is trivial.

    At the end of the main loop when drawing lines and circles you call

    glStencilMask(0x00);
    

    so at the beginning of the main loop clearing stencil buffer by

    glClearStencil(0);
    

    does nothing.

    Solution is to enable writing to stencil buffer by glStencilMask(0xFF); before the stencil buffer is cleared.