将鼠标坐标转换为 OpenGL 时出现错误偏移

Converting mouse coordinates to OpenGL is erroneously offset

我正在尝试使用鼠标实现对球体表面顶点的拾取。我正在使用 freeglut 和 OpenGL 4.5。

渲染

显示函数:

void display(void) {

    // Clear display port
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();

    // Reset camera
    gluLookAt(
        0.0, 0.0, 5.0,
        0.0, 0.0, 0.0,
        0.0, 1.0, 0.0
    );

    // Rotate
    glRotatef(-theta, 1.0, 0.0, 0.0);
    glRotatef(phi, 0.0, 0.0, 1.0);

    // Zoom
    glScalef(zoom, zoom, zoom);

    // Render sphere
    glPushMatrix();
    glTranslatef(0, 0, 0);
    glColor3f(1, 0, 0);
    glutWireSphere(1, 32, 32);
    glPopMatrix();

    .
    .
    .

    // Render selection point
    glPushMatrix();
    pointIsOnSphere ? glColor3f(0, 1, 0) : glColor3f(0, 0, 1);
    pointIsOnSphere ? glPointSize(5.0f) : glPointSize(2.5f);
    glBegin(GL_POINTS);
    glVertex3f(clickPosition.x, clickPosition.y, clickPosition.z);
    glEnd();
    glPopMatrix();

    // Swap buffers
    glutSwapBuffers();
}

整形函数:

void reshape(GLsizei width, GLsizei height) {
    if (height == 0) {
        height = 1;
    }
    float ratio = 1.0 * width / height;
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glViewport(0, 0, width, height);
    gluPerspective(45, ratio, 0.5, 5);
    glMatrixMode(GL_MODELVIEW);
}

鼠标处理

// Handles mouse click for point selection.
void mouse(int button, int state, int x, int y) {

    mouse_x = x;
    mouse_y = y;

    if (!pointIsOnSphere)
        return;

    if (button == GLUT_LEFT_BUTTON && state == GLUT_UP)
        controlPoints.push_back(clickPosition);

    if (button == GLUT_RIGHT_BUTTON && state == GLUT_DOWN)
        RIGHT_BUTTON_DOWN = true;

    glutPostRedisplay();
}

// Handles mouse movement for rotation.
void motion(int x, int y) {
    if (RIGHT_BUTTON_DOWN) {
        phi += (float)(x - mouse_x) / 4.0; 
        theta += (float)(mouse_y - y) / 4.0;
    }

    mouse_x = x;
    mouse_y = y;
    glutPostRedisplay();
}

    // Handles mouse movement for point selection.
void passiveMotion(int x, int y) {
    // Get position of click.
    clickPosition = GetOGLPos(x, y);
    // Set click position's z position to camera's z position.
    clickPosition.z = 1;
    // Create directional vector pointing into the screen.
    glm::vec3 into_screen = glm::vec3(0, 0, -1);
    // Create ray.
    ray r = ray(
        clickPosition,
        into_screen
    );

    mousePosition = clickPosition;
    float t = hit_sphere(glm::vec3(0), 1, r);
    if (t != -1) {
        pointIsOnSphere = true;
        clickPosition += t * into_screen;
    }
    else {
        pointIsOnSphere = false;
    }

    glutPostRedisplay();
}

// Handles scroll input for zoom.
void mouseWheel(int button, int state, int x, int y) {
    if (state == 1) {
        zoom = zoom + zoom_sensitivity >= zoom_max ? zoom_max : zoom + zoom_sensitivity;
    }
    else if (state == -1) {
        zoom = zoom - zoom_sensitivity <= zoom_min ? zoom_min : zoom - zoom_sensitivity;
    }
    glutPostRedisplay();
}

3D 采摘

// Get position of click in 3-d space
glm::vec3 GetOGLPos(int x, int y)
{
    GLint viewport[4];
    GLdouble modelview[16];
    GLdouble projection[16];
    GLfloat winX, winY, winZ;
    GLdouble posX, posY, posZ;

    glGetDoublev(GL_MODELVIEW_MATRIX, modelview);
    glGetDoublev(GL_PROJECTION_MATRIX, projection);
    glGetIntegerv(GL_VIEWPORT, viewport);

    winX = (float)x;
    winY = (float)viewport[3] - (float)y;
    glReadPixels(x, int(winY), 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &winZ);

    gluUnProject(winX, winY, winZ, modelview, projection, viewport, &posX, &posY, &posZ);
    return glm::vec3(posX, posY, posZ);
}

float hit_sphere(const glm::vec3& center, float radius, const ray& r) {
    glm::vec3 oc = r.origin() - center;
    float a = dot(r.direction(), r.direction());
    float b = 2.0 * glm::dot(oc, r.direction());
    float c = glm::dot(oc, oc) - radius * radius;
    float discriminant = b * b - 4 * a*c;

    if (discriminant < 0.0) {
        return -1.0;
    }
    else {
        float numerator = -b - sqrt(discriminant);
        if (numerator > 0.0) {
            return numerator / (2.0 * a);
        }

        numerator = -b + sqrt(discriminant);
        if (numerator > 0.0) {
            return numerator / (2.0 * a);
        }
        else {
            return -1;
        }
    }
}

视觉效果

如您所见,实际选择点偏离光标位置。

这是显示错误的视频:

https://gfycat.com/graciousantiquechafer

很明显,z 值有问题,我可以看到当您远离中心轴时误差会变大。但我似乎无法弄清楚代码中确切的问题所在。

新错误

现在,点出现在远离球体表面的地方。

Perspective Projection 处,投影矩阵描述了从针孔相机看到的世界中的 3D 点到视口的 2D 点的映射。
观看体积是一个Frustum(截角金字塔),其中金字塔的顶部是观看者的位置。
如果从相机位置开始一条射线,则射线上的所有点都具有相同的 xy window 坐标(和 xy 归一化设备坐标),这些点只是具有不同的 "depth"(z 坐标) . view ray投射到viewport上就是一个点。

这说明你的假设是错误的,方向"into the screen"不是(0, 0, -1):

// Create directional vector pointing into the screen.
glm::vec3 into_screen = glm::vec3(0, 0, -1);

请注意,这对于 正交投影 是正确的,但对于 Perspective Projection 是错误的。

方向取决于window坐标。幸运的是,它不依赖于深度,window 坐标由鼠标位置给出。
要找到 "hits" 当前鼠标位置的射线,您必须计算射线上的 2 个点。
找到光线与近平面(深度 = 0.0)和远平面(深度)的交点。由于所有 = 1.0)

这意味着从相机位置开始并通过鼠标光标的射线上 2 个点的 window 坐标是:

point 1: (mouseX, height-mouseY, 0.0)
point 2: (mouseX, height-mouseY, 1.0)

适应GetOGLPos:

glm::vec3 GetOGLPos(int x, int y, float depth)
{
    GLint viewport[4];
    GLdouble modelview[16];
    GLdouble projection[16];
    GLfloat winX, winY, winZ;
    GLdouble posX, posY, posZ;

    glGetDoublev(GL_MODELVIEW_MATRIX, modelview);
    glGetDoublev(GL_PROJECTION_MATRIX, projection);
    glGetIntegerv(GL_VIEWPORT, viewport);

    winX = (float)x;
    winY = (float)viewport[3] - (float)y;
    winZ = depth;
    //glReadPixels(x, int(winY), 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &winZ);

    gluUnProject(winX, winY, winZ, modelview, projection, viewport, &posX, &posY, &posZ);

    return glm::vec3(posX, posY, posZ);
}

定义ray

void passiveMotion(int x, int y)
{
    glm::vec3 clickPositionNear = GetOGLPos(x, y, 0.0);
    glm::vec3 clickPositionFar = GetOGLPos(x, y, 1.0);

    glm::vec3 into_screen = glm::normalize(clickPositionFar - clickPositionNear);

    ray r = ray(
        clickPositionNear,
        into_screen
    );

    // [...]
}