Search code examples
c++qtopenglselectionframebuffer

OpenGL : object selection


I'm trying to implement object selection in OpenGL, but I have some problems.

enter image description here

As you can see from the figure, the selection works well in the first view, but when you rotate the camera, the object is not selected correctly.

I implemented the selection this way. At the click of the mouse, I draw the scene in a framebuffer, each object depicted with a different color (and also different from the background color), and then check if the pixel clicked has the color of some objects.

These are the relevant code snippets.

struct CubeData
{
    QMatrix4x4 model;
    bool selected;
};

QMap<QString, CubeData> cubes;

initializeGL

void initializeGL()
{
    /* ... */

    auto FindColor = [=] ()
    {
        QVector<GLubyte> color;
        bool flag = true;

        while (flag)
        {
            color = RandomColor();
            flag = false;

            if (color == RgbFromColorToByte(background))
            {
                flag = true;
                continue;
            }

            if (cubes.contains(RgbFromByteToString(color)))
            {
                flag = true;
                continue;
            }
        }

        return color;
    };

    // cubes
    float offset = 3.0f;
    float x = -1.0f;
    while (cubes.size() < 3)
    {
        CubeData cube;
        cube.model.translate(x++ * offset, 0, 0);
        cube.selected = false;
        auto color = FindColor();
        cubes[RgbFromByteToString(color)] = cube;
    }
}

paintGL

void paintGL()
{
    /* ... */

    // enable depth test
    glEnable(GL_DEPTH_TEST);
    // clear buffers
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // draw cubes
    for (auto cube : cubes)
    {
        DrawCube(cube);
    }

    update();
}

mousePressEvent

void mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
    {
        makeCurrent();
        glBindFramebuffer(GL_FRAMEBUFFER, addFBO(FBOIndex::TEST));
        {
            // enable depth test
            glEnable(GL_DEPTH_TEST);
            // clear buffers
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            // draw test cubes
            DrawTest();

            // test pixel

            QVector<GLubyte> pixel;
            pixel.resize(3);

            glReadPixels(lastX, lastY, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &(pixel[0]));

            for (auto key : cubes.keys())
            {
                cubes[key].selected = false;
            }

            QString key = RgbFromByteToString(pixel);
            if (cubes.contains(key))
            {
                cubes[key].selected = true;
            }
        }
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());
        doneCurrent();
    }
}

DrawCube

void DrawCube(CubeData cube)
{
    GLuint program = addProgram(ProgramIndex::CUBE);
    glUseProgram(program);

    glUniformMatrix4fv(glGetUniformLocation(program, "model"), 1, GL_FALSE, cube.model.constData());
    glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, view.constData());
    glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 1, GL_FALSE, projection.constData());

    glUniform1i(glGetUniformLocation(program, "cube.selected"), cube.selected);

    glDrawArrays(GL_POINTS, 0, 1);
}

DrawTest

void DrawTest()
{
    GLuint program = addProgram(ProgramIndex::TEST);
    glUseProgram(program);

    glUniformMatrix4fv(glGetUniformLocation(program, "view"), 1, GL_FALSE, view.constData());
    glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 1, GL_FALSE, projection.constData());

    for (auto key : cubes.keys())
    {
        glUniformMatrix4fv(glGetUniformLocation(program, "model"), 1, GL_FALSE, cubes[key].model.constData());
        glUniform3fv(glGetUniformLocation(program, "cube.TestColor"), 1, RgbFromStringToFloat(key).constData());

        glDrawArrays(GL_POINTS, 0, 1);
    }
}

cube geometry shader

#version 450 core

struct Cube
{
    bool selected;
};

layout (points) in;
layout (triangle_strip, max_vertices = 14) out;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform Cube cube;

out FragData
{
    smooth vec3 color;
} frag;

void main()
{
    vec3 offset[14];
    offset[0x0] = vec3(-1.0f, +1.0f, +1.0f);
    offset[0x1] = vec3(+1.0f, +1.0f, +1.0f);
    offset[0x2] = vec3(-1.0f, -1.0f, +1.0f);
    offset[0x3] = vec3(+1.0f, -1.0f, +1.0f);
    offset[0x4] = vec3(+1.0f, -1.0f, -1.0f);
    offset[0x5] = vec3(+1.0f, +1.0f, +1.0f);
    offset[0x6] = vec3(+1.0f, +1.0f, -1.0f);
    offset[0x7] = vec3(-1.0f, +1.0f, +1.0f);
    offset[0x8] = vec3(-1.0f, +1.0f, -1.0f);
    offset[0x9] = vec3(-1.0f, -1.0f, +1.0f);
    offset[0xA] = vec3(-1.0f, -1.0f, -1.0f);
    offset[0xB] = vec3(+1.0f, -1.0f, -1.0f);
    offset[0xC] = vec3(-1.0f, +1.0f, -1.0f);
    offset[0xD] = vec3(+1.0f, +1.0f, -1.0f);

    mat4 MVP = projection * view * model;

    vec3 albedo = cube.selected ? vec3(1.0f, 0.0f, 0.0f) : vec3(1.0f);
    // grayscale weights
    vec3 weight = vec3(0.2126f, 0.7152f, 0.0722f);
    // shade power
    float power = 0.5f;

    for (int i = 0; i < 14; i++)
    {
        gl_Position = MVP * vec4(offset[i], 1.0f);

        float shade = dot((normalize(offset[i]) + 1.0f) * 0.5f, weight);
        frag.color = albedo * (shade + (1.0f - shade) * power);

        EmitVertex();
    }

    EndPrimitive();
}

test geometry shader

#version 450 core

struct Cube
{
    vec3 TestColor;
};

layout (points) in;
layout (triangle_strip, max_vertices = 14) out;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

uniform Cube cube;

out FragData
{
    flat vec3 color;
} frag;

void main()
{
    vec3 offset[14];
    offset[0x0] = vec3(-1.0f, +1.0f, +1.0f);
    offset[0x1] = vec3(+1.0f, +1.0f, +1.0f);
    offset[0x2] = vec3(-1.0f, -1.0f, +1.0f);
    offset[0x3] = vec3(+1.0f, -1.0f, +1.0f);
    offset[0x4] = vec3(+1.0f, -1.0f, -1.0f);
    offset[0x5] = vec3(+1.0f, +1.0f, +1.0f);
    offset[0x6] = vec3(+1.0f, +1.0f, -1.0f);
    offset[0x7] = vec3(-1.0f, +1.0f, +1.0f);
    offset[0x8] = vec3(-1.0f, +1.0f, -1.0f);
    offset[0x9] = vec3(-1.0f, -1.0f, +1.0f);
    offset[0xA] = vec3(-1.0f, -1.0f, -1.0f);
    offset[0xB] = vec3(+1.0f, -1.0f, -1.0f);
    offset[0xC] = vec3(-1.0f, +1.0f, -1.0f);
    offset[0xD] = vec3(+1.0f, +1.0f, -1.0f);

    mat4 MVP = projection * view * model;

    for (int i = 0; i < 14; i++)
    {
        gl_Position = MVP * vec4(offset[i], 1.0f);
        frag.color = cube.TestColor;
        EmitVertex();
    }

    EndPrimitive();
}

I also tried to view the entire test framebuffer by saving its contents in PPM format, but this is what I get.

enter image description here

Here's how I generated the PPM image.

mousePressEvent

void mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
    {
        makeCurrent();
        glBindFramebuffer(GL_FRAMEBUFFER, addFBO(FBOIndex::TEST));
        {
            /* ... */

            int w = geometry().width();
            int h = geometry().height();

            QVector<GLubyte> frame;
            frame.resize(w * h * 3);

            glReadPixels(0, 0, w, h, GL_RGB, GL_UNSIGNED_BYTE, &(frame[0]));

            QString name = "test.ppm";
            QFile file(name);
            file.open(QIODevice::WriteOnly);
            {
                QTextStream stream(&file);
                stream << "P3" << endl;
                stream << w << " " << h << endl;
                stream << "255" << endl;

                int i = 0;
                QVector<GLubyte> pixel;

                while (!(pixel = frame.mid(i++ * 3, 3)).isEmpty())
                {
                    stream << pixel[0] << " " << pixel[1] << " " << pixel[2] << endl;
                }
            }
            file.close();
        }
        glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebufferObject());
        doneCurrent();
    }
}

Solution

  • I discovered the (silly) mistake I was making.

    The pixel coordinates returned by QMouseEvent::pos() start at the upper left corner, while those accepted by glReadPixels start at the lower left corner.

    So, changing the call to glReadPixels in mousePressEvent this way

    glReadPixels(lastX, geometry().height() - lastY, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, &(pixel[0]));
    

    solved the problem.