如何修复缩放鼠标例程?
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)
- x/y:鼠标屏幕坐标
- w/h: window width/height
- f:滚动时从 0.1 变为 4 的因子 down/up
- left/right/bottom/top:用于计算新正交投影的值
我得到的结果真的很奇怪,但我不知道我弄乱了公式的哪一部分。
能否请您找出我数学的哪一部分是错误的,或者只是 post 我可以尝试的清晰的伪代码?仅作记录,我已经阅读并测试了互联网上的很多版本,但还没有找到任何地方可以正确解释这个主题。
Ps。您不需要 post 任何与此主题相关的 SO link,因为我已经阅读了所有内容:)
我将根据以下一组假设以一般方式回答这个问题:
- 您使用矩阵
P
作为(正交)投影来描述您眼睛的实际映射 space 视图体积到标准视图体积 [-1,1]^3
OpenGL 将裁剪(参见还假设 2) 和一个用于视图变换的矩阵 V
,即 "camera" 的位置和方向(如果有这样的事情,尤其是在正射投影中)并基本上建立 eye space 相对于您的视图体积的定义。
- 我将忽略同质剪辑 space,因为您只使用完全仿射正射投影,这意味着 NDC 坐标和剪辑 space 将相同,并且没有任何技巧
w
应用坐标。
- 我假设眼睛 space 和投影矩阵的默认 GL 约定,特别是眼睛 space 原点是相机位置,相机注视方向是
-z
- 视口完全填满 window。
- Windows Space 是默认的 OpenGL 约定,其中原点位于 底部 左侧。
- 鼠标坐标在一些window特定的坐标系中,原点在顶部左边,鼠标在整数像素坐标。
- 我假设
P
定义的视图体积是对称的:right = -left
和 top = -bottom
,并且在缩放操作后它也应该保持对称,因此,以补偿对于任何移动,视图矩阵 V
也必须进行调整。
你要得到的是一个缩放,使鼠标光标下的物点不动,成为缩放操作的中心。鼠标光标本身只是 2D,而 3D space 中的一整条 直线 将映射到相同的像素位置。然而,在正射投影中,该线将与图像平面正交,因此我们无需过多关注三维。
所以我们想要的是用 P_old
缩放当前情况(由正交参数 l_old
、r_old
、b_old
、t_old
定义,n_old
和 f_old
) 和 V_old
(由 "camera" 位置 c_old
和方向 o_old
定义)由缩放因子 s
在鼠标位置 (x,y)
(在假设 6 的 space 中)。
我们可以直接看到一些东西:
- 投影的近平面和远平面应该不受操作影响,所以
n_new = n_old
和f_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
如果只用 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 不再需要作为副产品,因此它直接适用于任意正交矩阵的一般情况)。
使用这种方法,缩放操作按预期工作:
我正在尝试学习如何使用正交投影向鼠标方向缩放,到目前为止我已经知道了:
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)
- x/y:鼠标屏幕坐标
- w/h: window width/height
- f:滚动时从 0.1 变为 4 的因子 down/up
- left/right/bottom/top:用于计算新正交投影的值
我得到的结果真的很奇怪,但我不知道我弄乱了公式的哪一部分。
能否请您找出我数学的哪一部分是错误的,或者只是 post 我可以尝试的清晰的伪代码?仅作记录,我已经阅读并测试了互联网上的很多版本,但还没有找到任何地方可以正确解释这个主题。
Ps。您不需要 post 任何与此主题相关的 SO link,因为我已经阅读了所有内容:)
我将根据以下一组假设以一般方式回答这个问题:
- 您使用矩阵
P
作为(正交)投影来描述您眼睛的实际映射 space 视图体积到标准视图体积[-1,1]^3
OpenGL 将裁剪(参见还假设 2) 和一个用于视图变换的矩阵V
,即 "camera" 的位置和方向(如果有这样的事情,尤其是在正射投影中)并基本上建立 eye space 相对于您的视图体积的定义。 - 我将忽略同质剪辑 space,因为您只使用完全仿射正射投影,这意味着 NDC 坐标和剪辑 space 将相同,并且没有任何技巧
w
应用坐标。 - 我假设眼睛 space 和投影矩阵的默认 GL 约定,特别是眼睛 space 原点是相机位置,相机注视方向是
-z
- 视口完全填满 window。
- Windows Space 是默认的 OpenGL 约定,其中原点位于 底部 左侧。
- 鼠标坐标在一些window特定的坐标系中,原点在顶部左边,鼠标在整数像素坐标。
- 我假设
P
定义的视图体积是对称的:right = -left
和top = -bottom
,并且在缩放操作后它也应该保持对称,因此,以补偿对于任何移动,视图矩阵V
也必须进行调整。
你要得到的是一个缩放,使鼠标光标下的物点不动,成为缩放操作的中心。鼠标光标本身只是 2D,而 3D space 中的一整条 直线 将映射到相同的像素位置。然而,在正射投影中,该线将与图像平面正交,因此我们无需过多关注三维。
所以我们想要的是用 P_old
缩放当前情况(由正交参数 l_old
、r_old
、b_old
、t_old
定义,n_old
和 f_old
) 和 V_old
(由 "camera" 位置 c_old
和方向 o_old
定义)由缩放因子 s
在鼠标位置 (x,y)
(在假设 6 的 space 中)。
我们可以直接看到一些东西:
- 投影的近平面和远平面应该不受操作影响,所以
n_new = n_old
和f_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
如果只用 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 不再需要作为副产品,因此它直接适用于任意正交矩阵的一般情况)。
使用这种方法,缩放操作按预期工作: