You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenGL中如何将光标屏幕坐标转换为世界坐标(按钮交互场景)

屏幕坐标转世界坐标实现OpenGL按钮悬停判断

核心思路

要判断光标是否悬停在按钮上,需要将GLFW获取的屏幕窗口坐标转换为OpenGL的世界空间坐标射线,再通过射线与按钮模型的碰撞检测来判断悬停状态。转换流程为:屏幕坐标 → 标准化设备坐标(NDC) → 观察空间坐标 → 世界空间坐标。

步骤拆解

  1. 屏幕坐标转NDC
    GLFW的光标坐标原点在窗口左上角,y轴向下;而NDC范围是[-1,1],原点在屏幕中心,y轴向上。转换公式:
float ndc_x = (2.0f * cursor_x) / window_width - 1.0f;
float ndc_y = 1.0f - (2.0f * cursor_y) / window_height;

将NDC的z值设为-1(近平面)和1(远平面),组合成齐次坐标用于后续逆变换。

  1. 计算逆矩阵
    要从NDC反推世界坐标,需要用到投影矩阵的逆矩阵和视图矩阵的逆矩阵——因为OpenGL的MVP变换是「模型→视图→投影」,逆变换则是「投影逆→视图逆」。

  2. 生成世界空间射线
    屏幕上的一个点对应相机发出的一条射线,通过近平面和远平面的NDC点逆变换得到世界空间的两个点,进而得到射线的起点(相机位置)和方向。

  3. 碰撞检测
    用生成的射线与按钮的包围盒(或模型网格)进行碰撞检测,判断是否悬停。

示例代码

假设你已经用GLM库处理矩阵,且已有投影矩阵projection、视图矩阵view,以及全局变量is_button_hovered标记悬停状态:

#include <glm/glm.hpp>
#include <glm/gtc/matrix_inverse.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>

bool is_button_hovered = false;
glm::mat4 projection; // 你的投影矩阵(比如glm::perspective或glm::ortho生成)
glm::mat4 view;       // 你的视图矩阵(比如glm::lookAt生成)

void cursorPosCallback(GLFWwindow* window, double xpos, double ypos) {
    int width, height;
    glfwGetWindowSize(window, &width, &height);

    // 1. 转换为标准化设备坐标(NDC)
    float ndc_x = (2.0f * static_cast<float>(xpos)) / width - 1.0f;
    float ndc_y = 1.0f - (2.0f * static_cast<float>(ypos)) / height;
    glm::vec4 ndc_near(ndc_x, ndc_y, -1.0f, 1.0f); // 近平面点
    glm::vec4 ndc_far(ndc_x, ndc_y, 1.0f, 1.0f);   // 远平面点

    // 2. 计算逆矩阵
    glm::mat4 inv_proj = glm::inverse(projection);
    glm::mat4 inv_view = glm::inverse(view);

    // 3. 转换为世界空间坐标并归一化齐次分量
    glm::vec4 world_near = inv_view * inv_proj * ndc_near;
    world_near /= world_near.w;
    glm::vec4 world_far = inv_view * inv_proj * ndc_far;
    world_far /= world_far.w;

    // 生成射线起点和方向
    glm::vec3 ray_origin = glm::vec3(world_near);
    glm::vec3 ray_dir = glm::normalize(glm::vec3(world_far) - ray_origin);

    // 4. 与按钮AABB包围盒碰撞检测(示例按钮在世界空间的包围盒)
    glm::vec3 button_min(-0.5f, -0.5f, 0.0f); // 按钮局部空间最小点(已转世界空间)
    glm::vec3 button_max(0.5f, 0.5f, 0.0f);   // 按钮局部空间最大点(已转世界空间)

    // AABB射线碰撞检测逻辑
    float t_min = (button_min.x - ray_origin.x) / ray_dir.x;
    float t_max = (button_max.x - ray_origin.x) / ray_dir.x;
    if (t_min > t_max) std::swap(t_min, t_max);

    float t_y_min = (button_min.y - ray_origin.y) / ray_dir.y;
    float t_y_max = (button_max.y - ray_origin.y) / ray_dir.y;
    if (t_y_min > t_y_max) std::swap(t_y_min, t_y_max);

    if ((t_min > t_y_max) || (t_y_min > t_max)) {
        is_button_hovered = false;
        return;
    }

    if (t_y_min > t_min) t_min = t_y_min;
    if (t_y_max < t_max) t_max = t_y_max;

    float t_z_min = (button_min.z - ray_origin.z) / ray_dir.z;
    float t_z_max = (button_max.z - ray_origin.z) / ray_dir.z;
    if (t_z_min > t_z_max) std::swap(t_z_min, t_z_max);

    if ((t_min > t_z_max) || (t_z_min > t_max)) {
        is_button_hovered = false;
        return;
    }

    // 碰撞成功,标记悬停
    is_button_hovered = true;
}

注意事项

  • 如果按钮有模型矩阵(平移、缩放、旋转),需要将按钮的包围盒通过模型矩阵转换到世界空间,或者将射线通过模型矩阵的逆转换到按钮局部空间再检测。
  • 示例中用的是AABB包围盒碰撞,若需要更高精度,可以改为检测按钮模型的三角形面。
  • 确保投影矩阵和视图矩阵是实时更新的(比如窗口大小变化时重新生成投影矩阵)。

内容的提问来源于stack exchange,提问作者yes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 05:45:34