Search code examples
c++openglfreeglutmouse-pickingqtopengl

Translating mouse coordinates to model coordinates in OpenGL when rotations are involved


In the Qt Forums I found this question and sparkled my curiosity.

I found a very simple example in Qt to display a cube and modified the cube logic to create a cube with a side length of 1 unit.

Then I tried to click on the model and show the coordinates of the area where I clicked.

When there are no rotations involved, it seems that the picking works fine. But! If I un-comment the rotations (or one of them) in paintGL I end up with "wrong" values.

For example:

  • No rotations and click almost on the leftmost border of the cube: the gluUnProject method says that I clicked on the point { -0.49075, 0.234, 0.5 }, which seems ok.

Cube without rotations

  • Rotations enabled and click almost on the leftmost border of the cube: I receive the point { -0.501456, 0.157555, -0.482942 }, which seems wrong. The x coordinate is off its range of -0.5, 0.5.

Cube with the two rotations enabled

I think the coordinates transformation is okay. I've been researching through Google and the people always use the same piece of code. Moreover, the color information of the pixel matches the color of the face where I clicked.

So, can anyone tell me why when a rotation is involved I get wrong coordinates? I think I'm failing in some basic 3D understanding, but I'm not able to realize where it is.

Here's the code:

main.cpp:

#include <QApplication>
#include "GLCube.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    GLCube w;
    w.resize(800,600);
    w.show();

    return a.exec();
}

GLCube.h

#ifndef GLCUBE_H
#define GLCUBE_H

#include <QtOpenGL>
#include <QGLWidget>

class GLCube : public QGLWidget{
  Q_OBJECT // must include this if you use Qt signals/slots
public:
    GLCube(QWidget *parent = NULL)
        : QGLWidget(parent) {
    }
protected:
    // Set up the rendering context, define display lists etc.:
   void initializeGL();
   // draw the scene:
   void paintGL();
   // setup viewport, projection etc.:
   void resizeGL (int width, int height);
   virtual void mousePressEvent(QMouseEvent *pme);
};

#endif

GLCube.cpp:

#include "GLCube.h"

#include <cmath>
#include <iostream>
#include <iomanip>

#include "GL/glu.h"

namespace {
    float ver[8][3] =
    {
        { 0.5, -0.5, 0.5 },
        { -0.5, -0.5, 0.5 },
        { -0.5, -0.5, -0.5 },
        { 0.5, -0.5, -0.5 },
        { 0.5, 0.5, 0.5 },
        { -0.5, 0.5, 0.5 },
        { -0.5, 0.5, -0.5 },
        { +0.5, 0.5, -0.5 }
    };
    GLfloat color[8][4] =
    {
        {0.0,0.0,0.0, 1.0},
        {1.0,0.0,0.0, 1.0 },
        {1.0,1.0,0.0, 1.0 },
        {0.0,1.0,0.0, 1.0 },
        {0.0,0.0,1.0, 1.0 },
        {1.0,0.0,1.0, 1.0 },
        {1.0,1.0,1.0, 1.0 },
        {0.0,1.0,1.0, 1.0 },
    };

    void quad(int a,int b,int c,int d, int col)
    {
        glPointSize( 5 );
        glBegin(GL_POINTS);
            glColor4fv(color[1]);
            glVertex3fv(ver[a]);

            glColor4fv(color[2]);
            glVertex3fv(ver[b]);

            glColor4fv(color[3]);
            glVertex3fv(ver[c]);

            glColor4fv(color[4]);
            glVertex3fv(ver[d]);
        glEnd();

        glBegin(GL_LINES);
            glColor4fv(color[1]);
            glVertex3fv(ver[a]);
            glVertex3fv(ver[b]);

            glColor4fv(color[1]);
            glVertex3fv(ver[b]);
            glVertex3fv(ver[c]);

            glColor4fv(color[1]);
            glVertex3fv(ver[c]);
            glVertex3fv(ver[d]);

            glColor4fv(color[1]);
            glVertex3fv(ver[d]);
            glVertex3fv(ver[a]);
        glEnd();

        glBegin(GL_QUADS);
        glColor4fv(/*color[a]*/ color[col] );
        glVertex3fv(ver[a]);

    //    glColor3fv(color[b]);
        glVertex3fv(ver[b]);

    //    glColor3fv(color[c]);
        glVertex3fv(ver[c]);

    //    glColor3fv(color[d]);
        glVertex3fv(ver[d]);
        glEnd();
    }

    void colorcube()
    {
//        glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
        quad( 3, 2, 1, 0, 1 ); // bottom
        quad( 7, 6, 5, 4, 2); // top
        quad( 6, 2, 1, 5, 3 ); // front
        quad( 7, 3, 0, 4, 5 ); // back
        quad( 7, 6, 2, 3, 6 ); // left
        quad( 4, 5, 1, 0, 7); // right
    }
}
/*
 * Sets up the OpenGL rendering context, defines display lists, etc.
 * Gets called once before the first time resizeGL() or paintGL() is called.
 */
void GLCube::initializeGL(){
    //activate the depth buffer
    glEnable(GL_DEPTH_TEST);
    qglClearColor(Qt::black);
    glEnable(GL_CULL_FACE);
}


/*
 *  Sets up the OpenGL viewport, projection, etc. Gets called whenever the widget has been resized
 *  (and also when it is shown for the first time because all newly created widgets get a resize event automatically).
 */
void GLCube::resizeGL (int width, int height){
    glViewport( 0, 0, (GLint)width, (GLint)height );
    /* create viewing cone with near and far clipping planes */
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glFrustum( -1.0, 1.0, -1.0, 1.0, 15.0, 30.0);

    glMatrixMode( GL_MODELVIEW );
}

/*
 * Renders the OpenGL scene. Gets called whenever the widget needs to be updated.
 */
void GLCube::paintGL(){

    //delete color and depth buffer
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
        glTranslatef(0.0f,0.0f,-20.0f); //move along z-axis
        glRotatef(30.0,0.0,1.0,0.0); //rotate 30 degress around y-axis
        glRotatef(15.0,1.0,0.0,0.0); //rotate 15 degress around x-axis

    colorcube();
//    originalcube();
}

void GLCube::mousePressEvent(QMouseEvent *pme) {
    GLint viewport[4];
    GLdouble modelview[16];
    GLdouble projection[16];
    glGetDoublev( GL_MODELVIEW_MATRIX, modelview );
    glGetDoublev( GL_PROJECTION_MATRIX, projection );
    glGetIntegerv( GL_VIEWPORT, viewport );

    const int x = pme->x();
    const int y = viewport[3] - pme->y();

    qDebug() << "HERE: " << x << y;

    GLfloat color[4];
    glReadPixels( x, y, 1, 1, GL_RGBA, GL_FLOAT, color);
    GLenum error = glGetError();
    std::cout << "RETRIEVED COLOR:" << color[0] << ", " << color[1] << ", " << color[2] << ", " << color[3] << std::endl;
    printf( "\tERROR: %s (Code: %u)\n", gluErrorString(error), error );
    if(GL_NO_ERROR != error) throw;

    GLdouble depthScale;
    glGetDoublev( GL_DEPTH_SCALE, &depthScale );
    std::cout << "DEPTH SCALE: " << depthScale << std::endl;
    GLfloat z;
    glReadPixels( x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &z );
    error = glGetError();
    std::cout << "X: " << x << ", Y: " << y  << ", RETRIEVED Z: " << z << std::endl;
    printf( "\tERROR: %s (Code: %u)\n", gluErrorString(error), error );
    if(GL_NO_ERROR != error) throw;
    std::cout << std::endl << std::endl;

    GLdouble posX, posY, posZ;
    GLint result;
    result = gluUnProject( x, y, z, modelview, projection, viewport, &posX, &posY, &posZ);
    error = glGetError();
    std::cout << "3D point with POS: " << posX << " " << posY << " " << posZ << std::endl;
    printf( "\tERROR: %s (Code: %u)\n", gluErrorString(error), error );
    std::cout << "\tglUnProject: " << (( GL_FALSE == result ) ? "FALSE" : "TRUE") << std::endl;
    if(GL_NO_ERROR != error) throw;
}

NOTE:

I'm running this in Windows 8.1 64 bit, Qt 4.8 and MinGW.

Also, both glReadPixels and gluUnProject exit without errors (ERROR CODE = GL_NO_ERROR )

And glut is not available. Only what OpenGL, QtOpenGL and/or glu offer.


Solution

  • It is not really wrong, you are reading the depth buffer to figure out the window-space Z value to use for reverse projection.

    The problem is that there is limited precision available in the depth buffer, and that introduces a bit of inaccuracy. In reality you cannot expect the range of unprojected values to be perfectly [-0.5,0.5]. You are going to have to introduce a small epsilon here, so your effective range would then be something like [-0.5015,0.5015].

    You could probably lessen the impact by increasing the precision of your depth buffer and/or decreasing the range between the near and far clip planes. The depth buffer is generally 24-bit fixed-point by default, but a 32-bit fixed-point or floating-point depth buffer might slightly improve your situation. However, you are never going to completely eliminate this problem.