《深度探讨透视投影坐标系》由会员分享,可在线阅读,更多相关《深度探讨透视投影坐标系(10页珍藏版)》请在金锄头文库上搜索。
1、3d 图形程序,就一定会做坐标变换。而谈到坐标变换,就不得不提起投影变换,因为它是所有变换中最不容易弄懂的。但有趣的是,各种关于透视变换的文档却依然是简之又简,甚至还有前后矛盾的地方。看来如此这般光景,想要弄清楚它,非得自己动手不可了。所以在下面的文章里,作者尝试推导一遍这个难缠的透视变换,然后把它套用到 DX 和 PS2lib 的实例中去。1.一般概念 所谓透视投影变换,就是 view 空间到 project 空间的带透视性质的坐标变换步骤(这两个空间的定义可以参考其他文档和书籍)。我们首先来考虑它应该具有那些变换性质。很显然,它至少要保证我们在 view 空间中所有处于可视范围内的点通过变
2、换之后,统统落在 project 空间的可视区域内。好极了,我们就从这里着手先来看看两个空间的可视区域。由于是透视变换,view 空间中的可见范围既是常说的视平截体(view frustum)。如图,(图 1)它就是由前后两个截面截成的这个棱台。从 view 空间的 x 正半轴看过去是下图这个样子。(图 2)接下来是 project 空间的可视范围。这个空间应当是处于你所见到的屏幕上。实际上将屏幕表面视作 project 空间的 xoy 平面,再加一条垂直屏幕向里(或向外)的 z 轴(这取决于你的坐标系是左手系还是右手系),这样就构成了我们想要的坐标系。好了,现在我们可以用视口(view po
3、rt)的大小来描述这个可视范围了。比如说全屏幕 640*480 的分辨率,原点在屏幕中心,那我们得到的可视区域为一个长方体,它如下图(a)所示。(图 3)但是,这样会带来一些设备相关性而分散我们的注意力,所以不妨先向 DirectX 文档学学,将 project 空间的可视范围定义为 x-1,1, y-1,1, z0,1的一个立方体(上图 b)。这实际上可看作一个中间坐标系,从这个坐标系到上面我们由视口得出的坐标系,只需要对三个轴向做一些放缩和平移操作即可。另外,这个 project 坐标系对 clip 操作来说,也是比较方便的。2.推导过程 先从 project 空间的 x 正半轴看看我们的
4、变换目标。(图 4)这个区域的上下边界为 y=1, 而图 2 中的上下边界为 y = z * tan(fov/2),要实现图2 到图 4 的变换,我们有 y = y * cot(fov/2) / z。这下完了,这是一个非线性变换,怎么用矩阵计算来完成呢?还好我们有 w 这个分量。注意到我们在做投影变换之前所进行的两次坐标变换world 变换和 view 变换,他们只是一系列旋转平移和缩放变换的叠加。仔细观察这些变换矩阵,你会发现它们其实不会影响向量的 w 分量。换句话说,只要不是故意,一个 w 分量等于 1 的向量,再来到投影变换之前他的 w 分量仍旧等于 1。好的,接下来我们让 w= w*z
5、, 新的 w 就记录下了view 空间中的 z 值。同时在 y 分量上我们退而求其次,只要做到 y = y * cot(fov/2)。那么,在做完线性变换之后,我们再用向量的 y 除以 w,就得到了我们想要的最终的 y 值。x 分量的变换可以如法炮制,只是 fov 要换一换。事实上,很多用以生成投影变换矩阵的函数都使用了 aspect 这个参数。这个参数给出了视平截体截面的纵横比(这个比值应与 view port 的纵横比相等,否则变换结果会失真)。如果我们按照惯例,定义 aspect = size of X / size of Y。那么我们就可以继续使用同一个 fov 而给出 x 分量的变换
6、规则:x = x * cot(fov/2) / aspect。现在只剩下 z 分量了。我们所渴望的变换应将 z = Znear 变换到 z = 0,将 z = Zfar 变换到 z = 1。这个很简单,但是等等,x, y 最后还要除以 w,你 z 怎能例外。既然也要除,那么 z = Zfar 就不能映射到 z = 1 了。唔,先映射到 z = Zfar 试试。于是,有 z = Zfar*(z-Znear)/(Zfar Znear)。接下来,看看 z/z 的性质。令 f(z) = z/z = Zfar*(z-Znear)/(z*(Zfar Znear))。则 f(z) = Zfar * Znea
7、r / ( z2 * (Zfar Znear ), 显而易见 f(z) 0。所以除了 z = 0 是一个奇点,函数 f(z)是一个单调增的函数。因此,当 ZnearzZfar 时,f(Znear)f(z)f(Zfar),即 0f(z)1。至此,我们可以给出投影变换的表达式了。x = x*cot(fov/2)/aspecty = y*cot(fov/2)z = z*Zfar / ( Zfar Znear ) Zfar*Znear / ( Zfar Znear )w = z以矩阵表示,则得到变换矩阵如下,cot(fov/2)/aspect 0 0 00 cot(fov/2) 0 00 0 Zfar
8、/(Zfar-Znear) 10 0 -Zfar*Znear/(Zfar-Znear) 0。做完线性变换之后,再进行所谓的“归一化”,即用 w 分量去除结果向量。现在我们考虑一下这个变换对全 view 空间的点的作用。首先是 x 和 y 分量,明了地,当 z0时,一切都如我们所愿;当 z0 时,x 和 y 的符号在变换前后发生了变化,从图象上来说,view 空间中处于 camera 后面的图形经过变换之后上下颠倒,左右交换;当 z= 0 时,我们得到的结果是无穷大。这个结果在实际中是没有意义的,以后我们得想办法弄掉它。再来看 z,仍旧拿我们上面定义的 f(z)函数来看,我们已经知道当 zZfa
9、r 时,f(z)1;同时当 z+,f(z)Zfar/(Zfar-Znear);当 z+0 时,f(z)-; z-0 时,f(z)+; z时,f(z)Zfar/(Zfar-Znear).由此我们画出 f(z)的图像。(图 5)由此图可以看出当 z0 时,如果我们仍旧使用 f(z)进行绘制会产生错误。所以我们会想需要 clip操作只要这个三角形有任意一个顶点经过变换后 z 值落在Zfar/(Zfar-Znear), +区间中,我们就毫不怜悯地抛弃她因为无论如何,这个结果是错的。那么万一有三角形在 view 空间内横跨了 Znear 到 0 的范围,按我们想应该是画不出来了。但是回想一下我们所看见过
10、的 DirectX程序,似乎从未看到过这种情况。有点奇怪,但是不得不先放放,稍后再说。3到 DirectX 中求证在 DirectX 中拿一个用 fov 生成投影矩阵的函数来看。D3DXMATRIX* D3DXMatrixPerspectiveFovLH( D3DXMATRIX* pOut, FLOAT fovy, FLOAT Aspect,FLOAT zn, FLOAT zf )这个函数恰好使用了我们刚才推导所使用的几个参数,经过一些数据的代入计算之后,我们就会发现它所产生的矩阵就是我们计算出来的。看来,DirectX 的思路和我们是一致的。好的,一个问题解决了,但一个新的问题接着产生Dir
11、ectX 是怎么做 clip 的?我不知道,而且看样子现在也知道不了,只能期待牛人相助或者是碰到一本好书了。4研究 ps2lib 的投影变换其实投影变换都是一回事,但是 PS2lib 的函数怎么有点不一样呢?仔细看看,原来我们的思路是先做“归一化”,然后再做 view port 的放缩和平移,而 PS2 不是这样它把“归一化”放在最后。接下来,我们就按这个顺序试试。先看缩放操作,把它和除 z 交换顺序很方便,直接换便是了。于是我们记 view port 的宽度为Vw,高度为 Vh, Z 缓存的最大值为 Zmax, 最小值为 Zmin 则有x = x * cot(fov/2)/aspect*(V
12、w/2)y = y * cot(fov/2)*(Vh/2)z = Zfar(z-Znear)/(Zfar-Znear) * (Zmax-Zmin);w = z再看平移部分,既然是要平移后再除,则必须平移原来的 z 倍,于是我们又记 view port 中心坐标为(Cx, Cy),就有x = x + z * Cxy = y + z * Cyz = z + z * Zminw = w好的,我们看看 cot(fov/2)等于什么,从图 2 看,实际上它就是 D/(Vh/2),那么 cot(fov/2)/aspect 实际上就是 D/(Vw/2)。但是,ps2 在这上面耍了个小花招,它在 view 空
13、间中的 view port和 project 空间的 view port 可以不相等。最明显的一点是,它在 view 空间中的 view port 的高度为 480,但实际上它的输出的 y 向分辨率只有 224。也就是说,ps2 想要输出纵横比等于电视机的图像,就必须在 y 向上再加一个缩放。这个缩放在我们的变换中体现在哪呢?就在 y = D/(Vh/2) * (Vhscr/2)中,注意到两个 Vh 不相等(project 空间中的 Vh 记成 Vhscr),两个值一运算就得到 x = D*(224/480) = 0.466667D。这个 0.4666667 就是 ps2lib 函数参数 ay
14、 的由来。同理,我们亦可得知 ax 一般应取值为 1。那么,实际上 ps2lib 函数的 scrz,ax, ay 三个参数的作用等同于 DirectX 的象形函数的 fov 和 aspect,在确定的规则下,他们可以相互转换,得到性质完全相同的透视变换。至于这个规则,这里就不给出了。转回正题,有了上面的讨论,我们就可以展开我们的变换表达式如下,x = x * scrz * ax + z * Cxy = x * scrz * ay + z * Cyz = z * (Zfar*ZmaxZnear*Zmin)/(Zfar Znear)Zfar*Znear*(Zmax-Zmin)/(Zfar-Znea
15、r)w = zz 分量好像还有点不一样,注意到一般 ps2 程序在 z buffer 的操作为 greater&equal,而 DirectX 的操作为 less&equal,就是说,z 方向得做些变动得把 z=Znear 映射到 z = Zmax,z=Zfar 映射到 z=Zmin。说变就变,我们马上有z = Zfar(z-Znear)/(Zfar-Znear)*(Zmin-Zmax)z = z+Zmax再次展开,得到 z = z * (Zfar*ZminZnear*Zmax)/(Zfar Znear )+ Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)好了,用矩阵把
16、这个变换写出来,scrz*ax 0 0 00 scrz*ay 0 0Cx Cy (Zfar*ZminZnear*Zmax)/(Zfar Znear ) 10 0 Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear) 0,这下就完全一样了。下面的任务就是看看这个变换的性质。因为最后同样要除以 z,所以 x,y 分量上的情形的和原来我们推导的 DirectX 的投影变换是一样的,区别在 z 分量上。来看新的 f(z)函数,它的图像为(图 6)5结论至此,我们已经完成了预定的目标。但是,将坐标变换完全掌握之后,为了做一个像样的图形程序,我们还有更多事情要做至少在 PS2 上是这样 。