3D 中的完美平移(也包括缩放)






4.91/5 (11投票s)
如何在透视视图中进行平移,使光标下的点保持在光标下。
引言
阅读本文的每个人都会熟悉 2D 图像的“抓取”式平移,就像 Adobe Acrobat® 一样,鼠标抓取图像并将其拖动。光标下的点保持在光标下。对于 3D,许多程序为平行(正交)视图提供了这种直观的交互,但我不知道有任何程序将其用于透视视图。一些 3D 程序确实会在 3D 中的构造类型平面上移动对象,使平面上的点保持在鼠标下方。这里展示的平移技术更具通用性。
通用视图参数
演示程序源自 A New Perspective on Viewing 中的演示程序,该程序提出了一组通用的视图参数。所有交互式运动算法都需要使用视图参数将基于屏幕的鼠标运动转换为虚拟空间运动。从上一篇文章中,定义视图空间中视图体积的七个通用视图参数是
struct TViewVolume { float hw, hh, zn, zf, iez, tsx, tsy; };
// These values are specified in view space
// hw - HalfWidth of cross section rectangle in z=0 plane
// hh - HalfHeight of cross section rectangle in z=0 plane
// zn - near clipping plane at z=zn
// zf - far clipping plane at z=zf
// iez - inverse of the eyepoint's z-coordinate (0 for parallel views)
// tsx - tan(SkewXAngle), typically 0
// tsy - tan(SkewYAngle), typically 0
// SkewXAngle and SkewYAngle are the angles between the axis of the view volume
// and the view space z-axis.
视图到世界的旋转/平移变换,它在世界空间中定位视图体积,完成了视图规范。
平移算法
平行视图平移相对简单,需要将鼠标像素运动缩放到虚拟空间。在透视视图中,离视点越近的物体在屏幕上的移动速度越快,而离视点越远的物体移动速度越慢。为了使拾取的点保持在光标下方,需要将物理像素运动映射到穿过拾取点的相应虚拟平面。
z 缓冲器值是拾取点的屏幕空间 z 坐标。从屏幕(深度缓冲器)空间到视图空间的逆变换将屏幕空间点映射到视图空间。演示的源代码展示了如何在 OpenGL 和 Direct3D 中读取深度缓冲器值。z 缓冲器值提供的值在 0.0(近)和 1.0(远)之间。逆变换公式的推导在Main.cpp 中的 OnPicked()
方法中作为注释给出。
// Transform the screen space z-coordinate to view space
float m33 = -(1-vv.zf*vv.iez) / (vv.zn-vv.zf);
ViewZ = (ScreenZ+m33*vv.zn) / (ScreenZ*vv.iez+m33);
MotionZ = ViewZ; // save for repeated use
计算像素到视图矩形的比例因子,并用于将物理鼠标 2D 点映射到视图空间 z=0 平面中的虚拟 2D 点。然后将 2D 点投影到 z=MotionZ 平面上。
// Calculate the pixel-to-view-rectangle scale factor
RECT rect;
GetClientRect( hWnd, &rect );
float PixelToViewRectFactor = vv.hw * 2.0f / rect.right;
// Calculate the 2D picked point on the view space z=0 plane.
// Note: Screen +Y points down.
ViewX = ScreenX * PixelToViewRectFactor - vv.hw;
ViewY = -(ScreenY * PixelToViewRectFactor - vv.hh);
// Now project the 2D point from the z=0 plane to the z=MotionZ plane
ViewX += -ViewX*MotionZ*vv.iez + vv.tsx*MotionZ;
ViewY += -ViewX*MotionZ*vv.iez + vv.tsy*MotionZ;
ViewZ = MotionZ;
rect.right
是窗口客户区的宽度(以像素为单位)。vv.hw
是 HalfWidth 通用视图参数,它指定了在视图空间 z=0 平面处视图体积矩形横截面的一半宽度。vv.hh
类似地是 HalfHeight 通用视图参数。
请注意计算如何同时处理平行(iez=0)和透视视图。这些公式是通用视图参数通常不需要代码来区分平行视图和透视视图的示例。有关通用视图参数的信息,请参阅 A New Perspective on Viewing。
对于每一次重复的鼠标移动,都会计算从 3D 视图空间的点移动到 3D 视图空间中的点,然后用于更新 ViewToWorld 平移。
// Calculate the view space pan vector, transform it to world space
// and subtract it from the ViewToWorld.trn
ViewToWorld.trn -= (MovedTo - MovedFrom) * ViewToWorld.rot;
ViewToWorld.trn
是视图到世界变换的平移部分,它是在世界空间中的一个点。同样,ViewToWorld.rot
是 3x3 旋转部分。通过减去向量来计算视图空间增量;然后通过乘以 ViewToWorld 旋转将视图空间增量转换为世界空间。最后,将世界空间增量添加到 ViewToWorld 平移中。重载的运算符实现了最后一个向量减法和向量乘以矩阵的操作。
读取 z 缓冲器值的代码有点复杂,因为演示程序会重绘屏幕,然后,在重绘之后,会读取深度缓冲器并将值通过回调函数传回。这种技术对于响应迅速的程序很有用,这些程序会在重绘后立即清除后缓冲器和 z 缓冲器,以最小化从读取最新输入值到显示更新图像的响应时间。
每次鼠标移动事件都会转换为鼠标移动增量,以便其他运动源(如动画运动)能够与鼠标无缝协作。平移算法将允许其他运动源将光标下的点移离光标,但这种交互对用户仍然是直观的。
缩放
缩放/旋转 Z 轴的交互也使拾取的点保持在光标下方。这种类型的交互类似于使用两根手指的 2D 多点触控屏交互,其中一根手指固定在窗口的中心。需要调整视图空间原点的 z 坐标以匹配拾取点的 z 坐标,这需要调整视图大小。所有其他计算都类似于平移的计算。
if (ISPERSPECTIVE(vv.iez) && ISPRESSED(GetKeyState( VK_SHIFT )))
{
float ez = 1/vv.iez;
HalfViewSize *= (ez-ViewZ)/ez; // scale the z=0 view rectangle
AVec3f Delta = CVec3f(0,0,ViewZ); // the view space translation
Delta = Delta * ViewToWorld.rot; // now world space
ViewToWorld.trn += Delta; // move the origin
ViewZ = 0; // move the picked point
InvalidateRect( hWnd, NULL, FALSE );
}
SpatialMath
向量和矩阵类型以及重载的运算符用于简化代码。有关更多信息,请参阅SpatialMath.h。
大多数用户更喜欢将缩放作为一项独立的动作而不是旋转。例如,垂直鼠标运动可用于放大或缩小视图,或放大或缩小对象。视图空间 z 坐标的使用同样适用于这种类型的缩放。
结论
由于完美的平移和缩放运动算法相对简单,令人惊讶的是这些算法尚未得到广泛应用。这种简单性源于通用视图参数的使用,而使用其他视图参数集可能难以实现这些算法。
在透视视图中进行完美平移是一种直观且潜意识的调整虚拟运动速率的方式,使 2D 图像的移动与鼠标的移动相匹配。用户将喜欢完美平移提供的直接控制。
历史
- 2009 年 10 月 12 日:首次发布。