OpenGL中如何将光标屏幕坐标转换为世界坐标(按钮交互场景)
屏幕坐标转世界坐标实现OpenGL按钮悬停判断
核心思路
要判断光标是否悬停在按钮上,需要将GLFW获取的屏幕窗口坐标转换为OpenGL的世界空间坐标射线,再通过射线与按钮模型的碰撞检测来判断悬停状态。转换流程为:屏幕坐标 → 标准化设备坐标(NDC) → 观察空间坐标 → 世界空间坐标。
步骤拆解
- 屏幕坐标转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(远平面),组合成齐次坐标用于后续逆变换。
计算逆矩阵
要从NDC反推世界坐标,需要用到投影矩阵的逆矩阵和视图矩阵的逆矩阵——因为OpenGL的MVP变换是「模型→视图→投影」,逆变换则是「投影逆→视图逆」。生成世界空间射线
屏幕上的一个点对应相机发出的一条射线,通过近平面和远平面的NDC点逆变换得到世界空间的两个点,进而得到射线的起点(相机位置)和方向。碰撞检测
用生成的射线与按钮的包围盒(或模型网格)进行碰撞检测,判断是否悬停。
示例代码
假设你已经用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
相关产品推荐
相关产品推荐

