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

在C++/OpenGL 2D游戏中获取纹理的GLFW像素坐标方法咨询

解决方案:纹理坐标到GLFW像素坐标的转换与点击检测实现

核心坐标系关系梳理

首先明确你用到的几个关键坐标系:

  • 局部空间:纹理自身的顶点坐标(如data.vertexPositions中的±0.5)
  • 世界空间:通过transform矩阵(平移、旋转、缩放)转换后的纹理实际位置
  • 裁剪空间/NDC:MVP矩阵变换后得到的标准化设备坐标,范围为[-1,1]
  • GLFW屏幕像素坐标:鼠标输入的坐标系,原点在窗口左上角,X轴向右、Y轴向下

步骤1:将纹理顶点转换为标准化设备坐标(NDC)

你可以复用现有的calculateMvpMatrix函数,传入目标纹理的position、rotation、scale参数,得到完整的MVP矩阵后,将纹理的四个局部顶点代入计算,再通过透视除法得到NDC坐标:

// 目标纹理的参数(从DrawQuad调用时的参数保存而来)
glm::vec3 texPos = {x, y, 0.0f};
float texRot = rotation;
glm::vec3 texScale = {size.x, size.y, 1.0f};
glm::mat4 mvp = calculateMvpMatrix(texPos, texRot, texScale);

// 纹理的四个局部顶点
std::vector<glm::vec4> localVertices = {
    glm::vec4(0.5f, -0.5f, 0.0f, 1.0f),
    glm::vec4(-0.5f, -0.5f, 0.0f, 1.0f),
    glm::vec4(-0.5f, 0.5f, 0.0f, 1.0f),
    glm::vec4(0.5f, 0.5f, 0.0f, 1.0f)
};

std::vector<glm::vec2> ndcVertices;
for (auto& v : localVertices) {
    glm::vec4 clipPos = mvp * v;
    // 透视除法得到NDC坐标
    glm::vec2 ndc = glm::vec2(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
    ndcVertices.push_back(ndc);
}

步骤2:NDC坐标转换为GLFW像素坐标

通过窗口宽高将NDC的[-1,1]范围映射到屏幕像素范围,同时注意反转Y轴(NDC的Y轴向上,GLFW的Y轴向下):

int windowWidth, windowHeight;
glfwGetWindowSize(window, &windowWidth, &windowHeight);

std::vector<glm::vec2> screenPixelCoords;
for (auto& ndc : ndcVertices) {
    float screenX = (ndc.x + 1.0f) * 0.5f * windowWidth;
    float screenY = (1.0f - ndc.y) * 0.5f * windowHeight;
    screenPixelCoords.push_back(glm::vec2(screenX, screenY));
}

此时screenPixelCoords中存储的就是纹理四个角对应的GLFW像素坐标。


步骤3:高效实现鼠标点击检测

逐像素检测效率较低,建议先做层级检测:

1. 轴对齐包围盒(AABB)快速筛选

用转换后的像素坐标计算纹理的最小/最大X、Y值,先判断鼠标点击是否在这个矩形范围内:

// 计算AABB边界
float minX = std::min({screenPixelCoords[0].x, screenPixelCoords[1].x, screenPixelCoords[2].x, screenPixelCoords[3].x});
float maxX = std::max({screenPixelCoords[0].x, screenPixelCoords[1].x, screenPixelCoords[2].x, screenPixelCoords[3].x});
float minY = std::min({screenPixelCoords[0].y, screenPixelCoords[1].y, screenPixelCoords[2].y, screenPixelCoords[3].y});
float maxY = std::max({screenPixelCoords[0].y, screenPixelCoords[1].y, screenPixelCoords[2].y, screenPixelCoords[3].y});

// 获取鼠标像素坐标
double mouseX, mouseY;
glfwGetCursorPos(window, &mouseX, &mouseY);

if (mouseX >= minX && mouseX <= maxX && mouseY >= minY && mouseY <= maxY) {
    // 进入下一步精确检测
}

2. 精确检测:鼠标坐标转世界空间对比

将鼠标像素坐标转换回世界空间,直接和纹理的世界空间范围对比(无需处理像素):

// 鼠标像素坐标转NDC
float ndcMouseX = (2.0f * mouseX) / windowWidth - 1.0f;
float ndcMouseY = 1.0f - (2.0f * mouseY) / windowHeight; // 反转Y轴
glm::vec4 ndcMouse = glm::vec4(ndcMouseX, ndcMouseY, 0.0f, 1.0f);

// 用MVP逆矩阵将NDC鼠标转世界空间
glm::mat4 invMVP = glm::inverse(mvp);
glm::vec4 worldMouse = invMVP * ndcMouse;
worldMouse /= worldMouse.w; // 透视除法

// 纹理的世界空间范围(局部±0.5经过缩放后的范围)
float halfWidth = texScale.x * 0.5f;
float halfHeight = texScale.y * 0.5f;
// 注意:如果纹理有旋转,需要用旋转后的边界判断,这里简化为无旋转情况
if (worldMouse.x >= texPos.x - halfWidth && worldMouse.x <= texPos.x + halfWidth &&
    worldMouse.y >= texPos.y - halfHeight && worldMouse.y <= texPos.y + halfHeight) {
    // 点击命中该纹理,可获取纹理名称
}

关键注意事项

  • 你的正交投影矩阵glm::ortho(0.0f, 8.0f, 0.0f, 4.5f,...)是固定范围,若窗口宽高比不是16:9会导致拉伸,建议改为和窗口宽高关联:
    float aspectRatio = (float)windowWidth / windowHeight;
    glm::mat4 projection = glm::ortho(0.0f, 8.0f, 0.0f, 8.0f / aspectRatio, 0.1f, 100.0f);
    
  • 需保存每个纹理的position、rotation、size参数,以便点击检测时重新计算MVP矩阵。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:05:00