我习惯于有些天真地解决这些用户交互问题(也许不是以数学上最优的方式),但考虑到它们对性能不是很关键(用户交互部分,不一定是对场景)。
对于对象的无约束自由拖动,您使用 unproject 描述的方法往往效果很好,通常只需稍作调整即可提供接近像素完美的拖动:
...而不是glReadPixels
用来尝试提取屏幕深度,当用户选择和/或选择时,您需要一个几何对象/网格的概念。现在只需投影该对象的中心点即可获得屏幕深度。然后,您可以在屏幕 X/Y 中拖动,保持与此投影相同的 Z,并取消投影以获得从先前中心到新中心的转换增量以转换对象。这也使它“感觉”就像您从对象的中心拖动一样,这往往非常直观。
对于自动约束拖动,一种快速检测方法是首先抓取“视平面法线”。使用您习惯的那些投影/非投影函数的一种快速方法(可能会让数学家皱眉)是在屏幕空间中取消投影视口中心的两个点(一个具有近 z 值,一个具有远 z 值)并得到这两个点之间的单位向量。现在您可以使用点积找到最接近该法线的世界轴。其他两个世界轴定义了我们要拖动的世界平面。
然后,再次使用那些方便的非投影函数沿着鼠标光标获得一条光线就变得很简单了。之后,您可以在四处拖动光标以计算增量的平移向量时进行重复的射线/平面相交。
对于更灵活的约束,小工具(又名操纵器,基本上是 3D 小部件)可以派上用场,以便用户可以根据小工具的哪些部分指示他想要什么样的拖动约束(平面、轴、无约束等)选择/拖动。对于轴约束,射线/线或线/线相交很方便。
根据评论中的要求,从视口中检索射线(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;
然后我们与从鼠标光标获得的射线进行射线/平面相交,以确定当用户开始拖动时射线与平面相交的位置(交点将为我们提供一个 3D 点)。之后,它只是通过在用户拖动鼠标时重复执行此操作收集的点之间的增量来平移对象。对象在沿平面约束移动时应直观地跟随鼠标。
轴拖动基本上是相同的想法,但是我们将光线变成一条线并进行线/线相交(将鼠标线对着线进行轴约束,给我们一个最近的点,因为这些线通常不会完美相交),给我们一个 3D 点,我们可以从中使用增量沿约束轴平移对象。
请注意,轴/平面拖动约束涉及一些棘手的边缘情况。例如,如果一个平面垂直于观察平面(或接近),它可以将物体射向无穷远。沿垂直线拖动轴时存在相同类型的情况,例如尝试从前视口(X/Y 观察平面)沿 Z 轴拖动。因此,值得检测线/平面垂直(或接近)的情况并防止在这种情况下拖动,但这可以在基本概念工作后完成。
在某些情况下,另一个值得注意的改善“感觉”方式的技巧是隐藏鼠标光标。例如,在轴约束下,鼠标光标最终可能会变得离轴本身很远,并且看起来/感觉很奇怪。所以我看到一些商业软件包在这种情况下只是简单地隐藏了鼠标光标,以避免显示鼠标和小工具/手柄之间的差异,结果往往感觉更自然一些。当用户释放鼠标按钮时,鼠标光标移动到手柄的视觉中心。请注意,您不应该为平板电脑执行此隐藏光标拖动(它们有点例外)。
这种拾取/拖动/相交的东西可能很难调试,因此值得一开始就解决它。为自己设定小目标,例如在某处的视口中单击鼠标按钮来创建射线。然后,您可以绕行并确保在正确的位置创建射线。接下来,您可以尝试一个简单的测试来查看该射线是否与世界平面(例如 X/Y)平面中的一个平面相交,并创建/可视化射线和平面之间的交点,并确保这是正确的。慢慢来,耐心的小步,调整自己的步调,你就会有平稳、自信的进步。尝试一次做太多事情,你可能会有非常令人沮丧的不和谐的进展,试图找出你哪里出错了。