在 OpenGL 中使用透视与正交投影时的光线投射(鼠标拾取)

Raycasting (Mouse Picking) while using an Perspective VS Orthographic Projection in OpenGL

本文关键字:光线投射 鼠标 投影 OpenGL 透视      更新时间:2023-10-16

我正在努力理解如何使用透视投影和正交投影更改我的算法来处理光线投射(用于鼠标拾取(。

目前,我有一个包含 3D 对象的场景,这些对象附加了轴对齐边界框。

在使用透视投影(使用 glm::p erspective 创建(渲染场景时,我可以成功地使用光线投射和鼠标来"拾取"场景中的不同对象。这是一个演示。

如果我渲染相同的场景,但使用正交投影,并将摄像机定位在朝下上方(向下看 Y 轴,想象就像游戏的关卡编辑器(,我无法从用户单击屏幕的位置正确进行光线投射,以便我可以在使用正交投影渲染时让 MousePick 工作。这是它不起作用的演示。

我的算法在高层次上:

auto const coords = mouse.coords();
glm::vec2 const mouse_pos{coords.x, coords.y};
glm::vec3 ray_dir, ray_start;
if (perspective) { // This "works"
auto const ar  = aspect_rate;
auto const fov = field_of_view;
glm::mat4 const proj_matrix = glm::perspective(fov, ar, f.near, f.far);
auto const& target_pos      =  camera.target.get_position();
glm::mat4 const view_matrix = glm::lookAt(target_pos, target_pos, glm::vec3{0, -1, 0});
ray_dir   = Raycast::calculate_ray_into_screen(mouse_pos, proj_matrix, view_matrix, view_rect);
ray_start = camera.world_position();
}
else if (orthographic) { // This "doesn't work"
glm::vec3 const POS     = glm::vec3{50};
glm::vec3 const FORWARD = glm::vec3{0, -1, 0};
glm::vec3 const UP      = glm::vec3{0, 0, -1};
// 1024, 768 with NEAR 0.001 and FAR 10000
//glm::mat4 proj_matrix = glm::ortho(0, 1024, 0, 768, 0.0001, 10000);
glm::mat4 proj_matrix = glm::ortho(0, 1024, 0, 768, 0.0001, 100);
// Look down at the scene from above  
glm::mat4 view_matrix = glm::lookAt(POS, POS + FORWARD, UP);
// convert the mouse screen coordinates into world coordinates for the cube/ray test
auto const p0 = screen_to_world(mouse_pos, view_rect, proj_matrix, view_matrix, 0.0f);
auto const p1 = screen_to_world(mouse_pos, view_rect, proj_matrix, view_matrix, 1.0f);
ray_start = p0;
ray_dir = glm::normalize(p1 - p0);
}
bool const intersects = ray_intersects_cube(logger, ray_dir, ray_start,
eid, tr, cube, distances);

在透视模式下,我们将光线投射到场景中,看看它是否与对象周围的立方体相交。

在正交模式下,我从屏幕上投射两条光线(一条在 z=0,另一条在 z=1 处(,并在这两点之间创建一条光线。我将光线起点设置为鼠标指针所在的位置(z=0(,并使用刚刚计算的光线方向作为同一ray_cube_intersection算法的输入。

我的问题是这个

由于鼠标拾取使用透视投影,但不使用正交投影:

  1. 假设相同的ray_cube相交算法可以用于透视/正交投影是否合理?
  2. 我关于在正字中设置ray_start和ray_dir变量的想法是否正确?

以下是正在使用的光线/立方体碰撞算法的来源。

glm::vec3
Raycast::calculate_ray_into_screen(glm::vec2 const& point, glm::mat4 const& proj,
glm::mat4 const& view, Rectangle const& view_rect)
{
// When doing mouse picking, we want our ray to be pointed "into" the screen
float constexpr Z            = -1.0f;
return screen_to_world(point, view_rect, proj, view, Z);
}
bool
ray_cube_intersect(Ray const& r, Transform const& transform, Cube const& cube,
float& distance)
{
auto const& cubepos = transform.translation;
glm::vec3 const                minpos = cube.min * transform.scale;
glm::vec3 const                maxpos = cube.max * transform.scale;
std::array<glm::vec3, 2> const bounds{{minpos + cubepos, maxpos + cubepos}};
float txmin = (bounds[    r.sign[0]].x - r.orig.x) * r.invdir.x;
float txmax = (bounds[1 - r.sign[0]].x - r.orig.x) * r.invdir.x;
float tymin = (bounds[    r.sign[1]].y - r.orig.y) * r.invdir.y;
float tymax = (bounds[1 - r.sign[1]].y - r.orig.y) * r.invdir.y;
if ((txmin > tymax) || (tymin > txmax)) {
return false;
}
if (tymin > txmin) {
txmin = tymin;
}
if (tymax < txmax) {
txmax = tymax;
}
float tzmin = (bounds[    r.sign[2]].z - r.orig.z) * r.invdir.z;
float tzmax = (bounds[1 - r.sign[2]].z - r.orig.z) * r.invdir.z;
if ((txmin > tzmax) || (tzmin > txmax)) {
return false;
}
distance = tzmin;
return true;
}

编辑:我正在使用的数学空间转换函数:

namespace boomhs::math::space_conversions
{
inline glm::vec4
clip_to_eye(glm::vec4 const& clip, glm::mat4 const& proj_matrix, float const z)
{
auto const      inv_proj   = glm::inverse(proj_matrix);
glm::vec4 const eye_coords = inv_proj * clip;
return glm::vec4{eye_coords.x, eye_coords.y, z, 0.0f};
}
inline glm::vec3
eye_to_world(glm::vec4 const& eye, glm::mat4 const& view_matrix)
{
glm::mat4 const inv_view  = glm::inverse(view_matrix);
glm::vec4 const ray       = inv_view * eye;
glm::vec3 const ray_world = glm::vec3{ray.x, ray.y, ray.z};
return glm::normalize(ray_world);
}
inline constexpr glm::vec2
screen_to_ndc(glm::vec2 const& scoords, Rectangle const& view_rect)
{
float const x = ((2.0f * scoords.x) / view_rect.right()) - 1.0f;
float const y = ((2.0f * scoords.y) / view_rect.bottom()) - 1.0f;
auto const assert_fn = [](float const v) {
assert(v <= 1.0f);
assert(v >= -1.0f);
};
assert_fn(x);
assert_fn(y);
return glm::vec2{x, -y};
}
inline glm::vec4
ndc_to_clip(glm::vec2 const& ndc, float const z)
{
return glm::vec4{ndc.x, ndc.y, z, 1.0f};
}
inline glm::vec3
screen_to_world(glm::vec2 const& scoords, Rectangle const& view_rect, glm::mat4 const& proj_matrix,
glm::mat4 const& view_matrix, float const z)
{
glm::vec2 const ndc   = screen_to_ndc(scoords, view_rect);
glm::vec4 const clip  = ndc_to_clip(ndc, z);
glm::vec4 const eye   = clip_to_eye(clip, proj_matrix, z);
glm::vec3 const world = eye_to_world(eye, view_matrix);
return world;
}
} // namespace boomhs::math::space_conversions

我为此工作了几天,因为我遇到了同样的问题。 我们习惯使用的取消投影方法在这里也 100% 正常工作 - 即使使用正交投影也是如此。但是对于正交投影,从摄像机位置到屏幕的方向矢量总是相同的。因此,在这种情况下,以相同的方式取消投影光标不会按预期工作。

您要做的是按原样获取相机方向矢量,但为了获得光线原点,您需要根据屏幕上的当前鼠标位置移动相机位置。

我的方法(C#,但你会明白的(:

Vector3 worldUpDirection = new Vector3(0, 1, 0); // if your world is y-up
// Get mouse coordinates (2d) relative to window position:
Vector2 mousePosRelativeToWindow = GetMouseCoordsRelativeToWindow(); // (0,0) would be top left window corner
// get camera direction vector:
Vector3 camDirection = Vector3.Normalize(cameraTarget - cameraPosition);
// get x and y coordinates relative to frustum width and height.
// glOrthoWidth and glOrthoHeight are the sizeX and sizeY values 
// you created your projection matrix with. If your frustum has a width of 100, 
// x would become -50 when the mouse is left and +50 when the mouse is right.
float x = +(2.0f * mousePosRelativeToWindow .X / viewportWidth  - 1) * (glOrthoWidth  / 2);
float y = -(2.0f * mousePosRelativeToWindow .Y / viewPortHeight - 1) * (glOrthoHeight / 2);
// Now, you want to calculate the camera's local right and up vectors 
// (depending on the camera's current view direction):
Vector3 cameraRight = Vector3.Normalize(Vector3.Cross(camDirection, worldUpDirection));
Vector3 cameraUp = Vector3.Normalize(Vector3.Cross(cameraRight, camDirection));
// Finally, calculate the ray origin:
Vector3 rayOrigin = cameraPosition + cameraRight * x + cameraUp * y;
Vector3 rayDirection = camDirection;

现在,您已经有了正交投影的光线原点和光线方向。 有了这些,您可以像往常一样运行任何射线平面/体积相交。