在 OpenGL 中使用鼠标选取在 3D space 中拖动 3D 点的最佳方法?

Best way to drag 3D point in 3D space with mouse picking in OpenGL?

用鼠标选取拖动 3D 点的最佳方法是什么。问题不在于拾取,而是在 3D 中拖动 space。

我想有两种方法,一种是使用 gluUnProject 获取 View to World 坐标并平移 3D 点。这种情况下的问题是,它仅在具有深度值(使用 glReadPixels)的表面上存在,如果鼠标离开表面,它会根据 gluUnProject 的 winZ 组件给出最大或最小深度值。而且它在某些情况下不起作用。

第二种方法是使用GL_MODELVIEW_MATRIX沿XY、XZ、YZ平面拖动。 但这种情况下的问题是,我们如何知道我们在 XY 或 XZ 或 YZ 平面上?怎么知道轨迹球的前视图是在XY平面,如果要在侧平面而不是前平面拖动怎么办?

那么,有没有什么方法可以给我准确的 2D 到 3D 坐标,这样我就可以在不考虑平面情况的情况下轻松地向各个方向拖动 3D 点?一定有办法的,我看过3D软件,拖拽功能很完善

我习惯于有些天真地解决这些用户交互问题(也许不是以数学上的最佳方式),但 "well enough" 考虑到它们对性能不是很关键(用户交互部分,不是必然是对场景的修改)。

对于对象的无约束自由拖动,您描述的使用 unproject 的方法往往工作得很好,通常只需稍作调整即可提供接近像素完美的拖动:

... 而不是使用 glReadPixels 来尝试提取屏幕深度,当用户选择 and/or 时,您需要几何 object/mesh 的概念。现在只需投影该对象的中心点即可获得屏幕深度。然后您可以在屏幕 X/Y 中四处拖动,保持从该投影获得的相同 Z,并取消投影以获得从先前中心到新中心的结果平移增量,以变换对象。这也使它 "feel" 就像您从对象的中心拖动一样,这往往非常直观。

对于自动约束拖动,一种快速检测方法是先抓取 'viewplane normal'。使用您习惯的 projection/unprojection 函数的一种快速方法(可能会让数学家皱眉)是取消投影屏幕空间中视口中心的两个点(一个具有近 z 值,一个具有远 z 值)并在这两点之间得到一个单位向量。现在您可以使用点积找到最接近法线的世界轴。另外两个世界轴定义了我们要拖动的世界平面。

然后再次使用那些得心应手的非投影函数沿着鼠标光标获取光线就变得很简单了。之后,您可以在拖动光标时重复 ray/plane 交叉点,以根据增量计算平移向量。

对于更灵活的约束,gizmo(又名操纵器,基本上是一个 3D 小部件)可以派上用场,这样用户就可以根据哪个指示他想要什么样的拖动约束(平面、轴、无约束等)小发明的一部分 he picks/drags。对于轴约束,ray/line 或 line/line 交集很方便。

根据评论中的要求,从视口检索光线(C++-ish 伪代码):

// Get a ray from the current cursor position (screen_x and screen_y).
const float near = 0.0f;
const float far = 1.0f;
Vec3 ray_org = unproject(Vec3(screen_x, screen_y, near));
Vec3 ray_dir = unproject(Vec3(screen_x, screen_y, far));
ray_dir -= ray_org;

// Normalize ray_dir (rsqrt should handle zero cases to avoid divide by zero).
const float rlen = rsqrt(ray_dir[0]*ray_dir[0] + 
                         ray_dir[1]*ray_dir[1] + 
                         ray_dir[2]*ray_dir[2]);
ray_dir[0] *= rlen;
ray_dir[1] *= rlen;
ray_dir[2] *= rlen;

然后我们与从鼠标光标获得的光线进行 ray/plane 交会,以确定当用户开始拖动时光线与平面相交的位置(交会将为我们提供 3D 点)。之后,它只是通过用户拖动鼠标时反复执行此操作收集的点之间的增量来平移对象。对象在沿平面约束移动时应该直观地跟随鼠标。

轴拖动基本上是相同的想法,但是我们把射线变成一条线并做一个line/line交叉(鼠标线对着轴约束的线,给我们一个最近的点,因为线通常不会完全相交),给我们返回一个 3D 点,我们可以从该点使用增量沿约束轴平移对象。

请注意,axis/planar 拖动约束涉及棘手的边缘情况。例如,如果一个平面与观察平面垂直(或接近),它可以将物体射向无穷远。轴沿垂直线拖动存在相同类型的情况,就像试图从前视口(X/Y 观察平面)沿 Z 轴拖动一样。因此,值得检测 line/plane 垂直(或接近)的情况并防止在这种情况下拖动,但这可以在您了解基本概念后完成。

在某些情况下 "feel" 改进方法的另一个值得注意的技巧是隐藏鼠标光标。例如,使用轴约束,鼠标光标最终可能会变得离轴本身很远,这可能 look/feel 很奇怪。因此,我看到许多商业软件包在这种情况下只是简单地隐藏了鼠标光标,以避免显示鼠标与 gizmo/handle 之间的差异,结果往往感觉更自然一些。当用户释放鼠标按钮时,鼠标光标移动到手柄的视觉中心。请注意,您不应该为平板电脑执行此隐藏光标拖动(它们有点例外)。

这个 picking/dragging/intersection 东西可能很难调试,所以值得逐步解决。为自己设定小目标,例如只需在视口某处单击鼠标按钮即可创建光线。然后你可以环绕并确保光线是在正确的位置创建的。接下来你可以尝试一个简单的测试,看看这条射线是否与世界上的一个平面相交(比如X/Y)平面,以及create/visualize射线和平面之间的交点,并确保它是正确的。循序渐进,耐心地逐步进行,调整好自己的节奏,你就会顺利、自信地进步。尝试一次做太多,你可能会在试图找出你哪里出错的过程中取得非常令人沮丧的不和谐进展。

这是一个非常有趣的主题。除了您描述的颜色标记方法外,为 z 缓冲区着色也是一种方法。这里有关于同一主题的类似讨论:

Generic picking solution for 3D scenes with vertex-shader-based geometry deformation applied

这也类似于已经充分讨论过的对象拾取,包括它们的优缺点: http://www.opengl-tutorial.org/miscellaneous/clicking-on-objects/picking-with-an-opengl-hack/

实际上,我的回答是没有唯一的最佳方法可以做到这一点。正如您还提到的,它们各有利弊。我个人喜欢 gluunproject,因为它简单而且结果还可以。此外,您不能使用屏幕 space 在任何方向移动顶点,因为屏幕 space 是 2D 并且没有唯一的方法将其映射回 3D 几何体 space。希望对您有所帮助:).