在2D图像处理中,对Adobe Acrobat等软件中的“抓取”式平移操作非常熟悉,即鼠标抓住图像并拖动它。在3D视图中,许多程序为平行(正交)视图提供了这种直观的交互方式,但似乎没有程序为透视视图实现它。一些3D程序确实允许在3D的构造型平面上移动对象,以便平面上的点保持在鼠标下方。这里展示的平移技术适用于更广泛的用途。
演示程序源自中的演示程序,该程序提出了一组通用视图参数。所有交互式运动算法都需要使用视图参数将基于屏幕的鼠标运动转换为虚拟空间运动。从之前的文章中,了解到七个通用视图参数指定了视图空间中的视图体积:
struct TViewVolume {
float hw, hh, zn, zf, iez, tsx, tsy;
};
// 这些值在视图空间中指定
// hw - z=0平面上横截面矩形的半宽
// hh - z=0平面上横截面矩形的半高
// zn - z=zn处的近裁剪平面
// zf - z=zf处的远裁剪平面
// iez - 视点z坐标的倒数(对于平行视图为0)
// tsx - SkewXAngle的正切值,通常为0
// tsy - SkewYAngle的正切值,通常为0
// SkewXAngle和SkewYAngle是视图体积轴和视图空间z轴之间的角度。
一个视图到世界的旋转/平移变换,将视图体积定位在世界空间中,完成了视图规范。
平行视图平移相对简单,需要将鼠标像素运动缩放到虚拟空间。在透视视图中,靠近视点的物体在屏幕上的平移速度比远离视点的物体快。为了保持选定点在鼠标下方,需要将物理像素运动映射到通过选定点的相应虚拟平面上。
z缓冲区值是选定点的屏幕空间z坐标。从屏幕(深度缓冲区)空间到视图空间的反向变换将屏幕空间点映射到视图空间。演示的源代码显示了如何在OpenGL和Direct3D中读取深度缓冲区值。z缓冲区值提供为介于0.0(前面)和1.0(后面)之间的值。反向变换公式的推导在Main.cpp中的OnPicked()方法中作为注释给出。
// 将屏幕空间z坐标转换为视图空间
float m33 = -(1 - vv.zf * vv.iez) / (vv.zn - vv.zf);
ViewZ = (ScreenZ + m33 * vv.zn) / (ScreenZ * vv.iez + m33);
MotionZ = ViewZ;
// 保存以供重复使用
计算像素到视图矩形的缩放因子,并将其用于将物理鼠标2D点映射到视图空间z=0平面上的虚拟2D点。然后将2D点投影到z=MotionZ平面上。
// 计算像素到视图矩形的缩放因子
RECT rect;
GetClientRect(hWnd, ▭);
float PixelToViewRectFactor = vv.hw * 2.0f / rect.right;
// 计算视图空间z=0平面上的2D选定点。
// 注意:屏幕+Y指向下方。
ViewX = ScreenX * PixelToViewRectFactor - vv.hw;
ViewY = -(ScreenY * PixelToViewRectFactor - vv.hh);
// 现在将2D点从z=0平面投影到z=MotionZ平面
ViewX += -ViewX * MotionZ * vv.iez + vv.tsx * MotionZ;
ViewY += -ViewX * MotionZ * vv.iez + vv.tsy * MotionZ;
ViewZ = MotionZ;
rect.right是窗口的客户区宽度(以像素为单位)。
vv.hw是通用视图参数,指定视图体积在z=0视图空间平面上的矩形横截面的一半宽度。vv.hh类似地是通用视图参数,指定视图体积在z=0视图空间平面上的矩形横截面的一半高度。
请注意,这些计算同时处理了平行(iez=0)和透视视图。这些公式是通用视图参数通常不需要代码来区分平行和透视视图的示例。有关通用视图参数的更多信息,请参见。
对于每次重复的鼠标移动,计算移动起点和终点的3D视图空间点,然后用于更新ViewToWorld.translation。
// 计算视图空间平移向量,将其转换为世界空间
// 并从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;
// 缩放z=0视图矩形
AVec3f Delta = CVec3f(0, 0, ViewZ);
// 视图空间平移
Delta = Delta * ViewToWorld.rot;
// 现在是世界空间
ViewToWorld.trn += Delta;
// 移动原点
ViewZ = 0;
// 移动选定点
InvalidateRect(hWnd, NULL, FALSE);
}
SpatialMath向量和矩阵类型以及重载操作符用于简化代码。有关更多信息,请参见。
大多数用户更喜欢将缩放作为单独的操作。例如,垂直鼠标运动可以用来放大和缩小视图或缩放对象。使用视图空间z坐标同样适用于这种类型的缩放。
完美的平移和缩放运动算法看起来相对简单,令人惊讶的是这些算法尚未广泛使用。这种简单性源于使用通用视图参数,而使用其他视图参数集实现这些算法可能会很困难。