如何修复缩放鼠标例程?

How to fix zoom towards mouse routine?

我正在尝试学习如何使用正交投影向鼠标方向缩放,到目前为止我已经知道了:

def dolly(self, wheel, direction, x, y, acceleration_enabled):
    v = vec4(*[float(v) for v in glGetIntegerv(GL_VIEWPORT)])
    w, h = v[2], v[3]
    f = self.update_zoom(direction, acceleration_enabled) # [0.1, 4]
    aspect = w/h
    x,y = x-w/2, y-h/2
    K1 = f*10
    K0 = K1*aspect

    self.left = K0*(-2*x/w-1)
    self.right = K0*(-2*x/w+1)
    self.bottom = K1*(2*y/h-1)
    self.top = K1*(2*y/h+1)

我得到的结果真的很奇怪,但我不知道我弄乱了公式的哪一部分。

能否请您找出我数学的哪一部分是错误的,或者只是 post 我可以尝试的清晰的伪代码?仅作记录,我已经阅读并测试了互联网上的很多版本,但还没有找到任何地方可以正确解释这个主题。

Ps。您不需要 post 任何与此主题相关的 SO link,因为我已经阅读了所有内容:)

我将根据以下一组假设以一般方式回答这个问题:

  1. 您使用矩阵 P 作为(正交)投影来描述您眼睛的实际映射 space 视图体积到标准视图体积 [-1,1]^3 OpenGL 将裁剪(参见还假设 2) 和一个用于视图变换的矩阵 V,即 "camera" 的位置和方向(如果有这样的事情,尤其是在正射投影中)并基本上建立 eye space 相对于您的视图体积的定义。
  2. 我将忽略同质剪辑 space,因为您只使用完全仿射正射投影,这意味着 NDC 坐标和剪辑 space 将相同,并且没有任何技巧 w 应用坐标。
  3. 我假设眼睛 space 和投影矩阵的默认 GL 约定,特别是眼睛 space 原点是相机位置,相机注视方向是 -z
  4. 视口完全填满 window。
  5. Windows Space 是默认的 OpenGL 约定,其中原点位于 底部 左侧。
  6. 鼠标坐标在一些window特定的坐标系中,原点在顶部左边,鼠标在整数像素坐标。
  7. 我假设 P 定义的视图体积是对称的:right = -lefttop = -bottom,并且在缩放操作后它也应该保持对称,因此,以补偿对于任何移动,视图矩阵 V 也必须进行调整。

你要得到的是一个缩放,使鼠标光标下的物点不动,成为缩放操作的中心。鼠标光标本身只是 2D,而 3D space 中的一整条 直线 将映射到相同的像素位置。然而,在正射投影中,该线将与图像平面正交,因此我们无需过多关注三维。

所以我们想要的是用 P_old 缩放当前情况(由正交参数 l_oldr_oldb_oldt_old 定义,n_oldf_old) 和 V_old(由 "camera" 位置 c_old 和方向 o_old 定义)由缩放因子 s在鼠标位置 (x,y)(在假设 6 的 space 中)。

我们可以直接看到一些东西:

  • 投影的近平面和远平面应该不受操作影响,所以n_new = n_oldf_new = f_old
  • 实际相机方向(或观察方向)也应该不受影响:o_new = o_old
  • 如果我们放大 s 倍,则实际视图体积必须缩放 1/s,因为当我们放大时,更小 整个世界的一部分在屏幕上比以前更清晰(并且看起来更大)。所以我们可以简单地缩放我们拥有的平截头体参数: l_new = l_old / sr_new = r_old / sb_new = b_old / st_new = t_old / s

如果只用 P_new 替换 P_old,我们得到了缩放,但是鼠标光标下的世界点会移动(除非鼠标恰好位于视图的中心)。所以我们必须通过修改相机位置来弥补这一点。

让我们先将鼠标坐标 (x,y) 放入 OpenGL window space(假设 5 和 6):

  • x_win = x + 0.5
  • y_win = height - 0.5 - y

请注意,除了镜像 y 之外,我还将坐标移动了半个像素。那是因为在 OpenGL window space 中,像素中心位于半坐标,而我假设您的整数鼠标坐标代表您点击的像素的中心(不会有太大区别视觉上,但仍然)

现在让我们进一步将坐标放入归一化设备Space(此处依赖假设 4):

  • x_ndc = 2.0 * x_win / width - 1
  • y_ndc = 2.0 * y_win / height - 1

根据假设 2,剪辑和 NDC 坐标将相同,我们可以将矢量 v 称为我们的 NDC/space 鼠标坐标:v = (x_ndc, y_ndc, 0, 1)^T

我们现在可以陈述我们的 "point under mouse must not move" 条件:

inverse(V_old) * inverse(P_old) * v = inverse(V_new) * inverse(P_new) * v

但让我们进入眼睛space,让我们看看发生了什么:

  • a = inverse(P_old) * v 成为我们缩放之前鼠标光标下的点的眼睛 space 位置。
  • b = inverse(P_new) * v 为缩放后鼠标光标下方指针的眼睛 space 位置。

由于我们假设了一个对称的视体积,我们已经知道对于 x 和 y 坐标,b = (1/s) *a 成立(假设 7。如果该假设不成立,您需要对 b 也是,这也不难)。

因此,我们可以设置一个 2D eye space 偏移向量 d 来描述我们的兴趣点是如何被比例尺移动的:

d = b - a = (1 / s) *a  - a = a (1/s - 1)

为了补偿该移动,我们必须反向移动相机,因此 -d

如果像我在假设 1 中所做的那样将相机位置分开,则只需相应地更新相机位置 c。你只需要注意 c 是世界 space 位置,而 d 是眼睛 space 偏移:

c_new = c_old - inverse(V_old) * (d_x, d_y, 0, 0)^T

不是说如果你不把camera position作为一个单独的变量,而是直接保留view matrix,你可以简单的pre-multiply the translation: V_new = translate(-d_x, -d_y, 0) * V_old

更新

到目前为止我写的是正确的,但我走了一条捷径,这在处理非无限精度数据类型时在数值上是一个非常糟糕的主意。如果缩小很多,相机位置的误差会很快累积。所以在@BPL 实现这个之后,这就是他得到的:

主要问题好像是我直接在eyespace中计算偏移向量d,没有取当前视图矩阵V_old(并考虑到它的小错误)。所以更稳定的方法是直接在world space:

中计算所有这些
a     = inverse(P_old * V_old) * v
b     = inverse(P_new * V_old) * v
d     = b - a
c_new = c_old - d

(这样做使得假设 7 不再需要作为副产品,因此它直接适用于任意正交矩阵的一般情况)。

使用这种方法,缩放操作按预期工作: