如何修复缩放鼠标例程?

BPL*_*BPL 2 opengl math 3d zooming

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

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)
Run Code Online (Sandbox Code Playgroud)
  • x/y:鼠标屏幕坐标
  • w/h:窗口宽度/高度
  • f:向下/向上滚动时从 0.1 到 4 的系数
  • 左/右/下/上:用于计算新正交投影的值

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

在此处输入图片说明

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

附言。您不需要发布与此主题相关的任何 SO 链接,因为我已经阅读了所有内容:)

der*_*ass 7

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

  1. 您使用P(正交)投影矩阵描述您的眼睛空间视图体积到标准视图体积的实际映射[-1,1]^3OpenGL 将根据(另请参见假设 2)和V视图变换矩阵,即视图的位置和方向“相机”(如果有这样的东西,尤其是在正射投影中)并基本上建立一个眼睛空间,您的视野体积将相对于该空间定义。
  2. 我将忽略齐次裁剪空间,因为您仅使用完全仿射正射投影,这意味着 NDC 坐标和裁剪空间将相同,并且不会对任何w坐标应用任何技巧。
  3. 我假设眼空间和投影矩阵的默认 GL 约定,特别是眼空间原点是相机位置,相机观察方向是 -z
  4. 视口正在完全填满窗口。
  5. 视窗空间是默认OpenGL的约定,其中原点位于底部的左侧。
  6. 鼠标坐标是在一些窗口特定坐标系,其中原点位于顶部左侧,鼠标是在整数像素坐标。
  7. 我假设由和定义的视图体积P是对称的,并且在缩放操作后它也应该保持对称,因此,为了补偿任何移动,也必须调整视图矩阵。right = -lefttop = -bottomV

你想要得到的是一个缩放,使得鼠标光标下的对象点不会移动,因此成为缩放操作的中心。鼠标光标本身只是 2D 的,3D 空间中的整条直线将被映射到相同的像素位置。但是,在正射投影中,该线将与图像平面正交,因此我们无需过多考虑第三维。

所以,我们要的是规模与目前的情况P_old(由邻参数定义l_oldr_oldb_oldt_oldn_oldf_old)和V_old(由“照相机”的位置定义c_old和ortientationo_old通过缩放因子)s在鼠标位置(x,y) (从假设6的空间)。

我们可以直接看到几点:

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

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

我们首先将鼠标坐标(x,y)放入 OpenGL 窗口空间(假设 5 和 6):

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

请注意,除了镜像 y 之外,我还将坐标移动了半个像素。那是因为在 OpenGL 窗口空间中,像素中心位于中间坐标,而我假设您的整数鼠标坐标将代表您单击的像素的中心(在视觉上不会有很大的不同,但仍然如此)

现在让我们进一步将坐标放入标准化设备空间(这里依赖于假设 4):

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

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

我们现在可以声明我们的“鼠标下的点不能移动”条件:

inverse(V_old) * inverse(P_old) * v = inverse(V_new) * inverse(P_new) * v
Run Code Online (Sandbox Code Playgroud)

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

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

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

因此,我们可以设置一个 2D眼空间偏移向量d,它描述了我们的兴趣点是如何被比例移动的:

d = b - a = (1 / s) *a  - a = a (1/s - 1)
Run Code Online (Sandbox Code Playgroud)

为了补偿这种移动,我们必须反向移动我们的相机,所以通过-d

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

c_new = c_old - inverse(V_old) * (d_x, d_y, 0, 0)^T
Run Code Online (Sandbox Code Playgroud)

并不是说如果您不将相机位置保留为单独的变量,而是直接保留视图矩阵,则可以简单地预先乘以平移: V_new = translate(-d_x, -d_y, 0) * V_old

更新

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

缩小很远时显示位置偏移的动画

主要的问题似乎是,我直接计算偏移向量d眼空间,不拿当前视图矩阵V_old(以及它的小误差考虑在内)。所以更稳定的方法是直接在世界空间中计算所有这些:

a     = inverse(P_old * V_old) * v
b     = inverse(P_new * V_old) * v
d     = b - a
c_new = c_old - d
Run Code Online (Sandbox Code Playgroud)

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

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

动画显示固定方法