zoukankan      html  css  js  c++  java
  • 深入探讨透视投影坐标变换

    OpenGL的透视变换,基本的理解是三个步骤(以glFrustum为例):

    1. 规定frustum,即视野范围;

    2. 进行切割;

    3. 将frustum压缩到canonical cube(2*2*2)中,这步又称为透视除法。

    转自:http://dev.gameres.com/Program/Visual/3D/shenruTS.htm

    深入探讨透视投影坐标变换
     

    写3d图形程序,就一定会做坐标变换。而谈到坐标变换,就不得不提起投影变换,因为它是所有变换中最不容易弄懂的。但有趣的是,各种关于透视变换的文档却依然是简之又简,甚至还有前后矛盾的地方。看来如此这般光景,想要弄清楚它,非得自己动手不可了。所以在下面的文章里,作者尝试推导一遍这个难缠的透视变换,然后把它套用到 DX和 PS2lib 的实例中去。

    1. 一般概念

    所谓透视投影变换,就是view 空间到project 空间的带透视性质的坐标变换步骤(这两

    个空间的定义可以参考其他文档和书籍)。我们首先来考虑它应该具有那些变换性质。很显然,它至少要保证我们在view空间中所有处于可视范围内的点通过变换之后,统统落在project空间的可视区域内。好极了,我们就从这里着手——先来看看两个空间的可视区域。

    由于是透视变换,view空间中的可见范围既是常说的视平截体(view frustum)。如图,

    (图1)

    它就是由前后两个截面截成的这个棱台。

    从view空间的x正半轴看过去是下图这个样子。

    (图2)

    接下来是project空间的可视范围。这个空间应当是处于你所见到的屏幕上。实际上将屏幕表面视作project空间的xoy平面,再加一条垂直屏幕向里(或向外)的z轴(这取决于你的坐标系是左手系还是右手系),这样就构成了我们想要的坐标系。好了,现在我们可以用视口(view port)的大小来描述这个可视范围了。比如说全屏幕640*480的分辨率,原点在屏幕中心,那我们得到的可视区域为一个长方体,它如下图(a)所示。

    (图3)

    但是,这样会带来一些设备相关性而分散我们的注意力,所以不妨先向DirectX文档学学,将project空间的可视范围定义为x∈[-1,1], y∈[-1,1], z∈[0,1]的一个立方体(上图b)。这实际上可看作一个中间坐标系,从这个坐标系到上面我们由视口得出的坐标系,只需要对三个轴向做一些放缩和平移操作即可。另外,这个project坐标系对clip操作来说,也是比较方便的。

    1. 推导过程

    先从project空间的x正半轴看看我们的变换目标。

    (图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, 新的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分量的变换规则: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 * Znear / ( z^2 * (Zfar –Znear )), 显而易见f’(z) > 0。所以除了z = 0是一个奇点,函数f(z)是一个单调增的函数。因此,当Znear≤zZfar时,f(Znear)≤f(z)≤f(Zfar),

    即0≤f(z)≤1。

    至此,我们可以给出投影变换的表达式了。

    x’ = x*cot(fov/2)/aspect

    y’ = y*cot(fov/2)

    z’ = z*Zfar / ( Zfar – Znear ) – Zfar*Znear / ( Zfar – Znear )

    w’ = z

    以矩阵表示,则得到变换矩阵如下,

    cot(fov/2)/aspect 0 0 0

    0 cot(fov/2) 0 0

    0 0 Zfar/(Zfar-Znear) 1

    0 0 -Zfar*Znear/(Zfar-Znear) 0。

    做完线性变换之后,再进行所谓的“归一化”,即用w分量去除结果向量。

      现在我们考虑一下这个变换对全view空间的点的作用。首先是x和y分量,明了地,当z>0时,一切都如我们所愿;当z<0时,x和y的符号在变换前后发生了变化,从图象上来说,view空间中处于camera后面的图形经过变换之后上下颠倒,左右交换;当z= 0 时,我们得到的结果是无穷大。这个结果在实际中是没有意义的,以后我们得想办法弄掉它。再来看z,

    仍旧拿我们上面定义的f(z)函数来看,我们已经知道当z≥Zfar时,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)

    由此图可以看出当z≤0时,如果我们仍旧使用f(z)进行绘制会产生错误。所以我们会想需要clip操作——只要这个三角形有任意一个顶点经过变换后z值落在[Zfar/(Zfar-Znear), +∞]区间中,我们就毫不怜悯地抛弃她——因为无论如何,这个结果是错的。那么万一有三角形在view空间内横跨了Znear到0的范围,按我们想应该是画不出来了。但是回想一下我们所看见过的DirectX程序,似乎从未看到过这种情况。有点奇怪,但是不得不先放放,稍后再说。

    3.到DirectX中求证

    在DirectX中拿一个用fov生成投影矩阵的函数来看。

    D3DXMATRIX* D3DXMatrixPerspectiveFovLH( D3DXMATRIX* pOut, FLOAT fovy, FLOAT Aspect,

    FLOAT zn, FLOAT zf )

    这个函数恰好使用了我们刚才推导所使用的几个参数,经过一些数据的代入计算之后,我们就会发现它所产生的矩阵就是我们计算出来的。看来,DirectX的思路和我们是一致的。好的,一个问题解决了,但一个新的问题接着产生——DirectX是怎么做clip的?我不知道,而且看样子现在也知道不了,只能期待牛人相助或者是碰到一本好书了。

    4.研究ps2lib的投影变换

      其实投影变换都是一回事,但是PS2lib的函数怎么有点不一样呢?仔细看看,原来我们的思路是先做“归一化”,然后再做view port的放缩和平移,而PS2不是这样——它把“归一化”放在最后。接下来,我们就按这个顺序试试。

    先看缩放操作,把它和除z交换顺序很方便,直接换便是了。于是我们记view port 的宽度为Vw,高度为Vh, Z缓存的最大值为Zmax, 最小值为Zmin则有

    x’ = x * cot(fov/2)/aspect*(Vw/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 * Cx

    y’’ = y’ + z * Cy

    z’’ = z’ + z * Zmin

    w’’ = w

    好的,我们看看cot(fov/2)等于什么,从图2看,实际上它就是D/(Vh/2),那么cot(fov/2)/aspect实际上就是D/(Vw/2)。但是,ps2在这上面耍了个小花招,它在view空间中的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的由来。同理,我们亦可得知ax一般应取值为1。那么,实际上ps2lib函数的scrz,ax, ay三个参数的作用等同于DirectX的象形函数的fov和aspect,在确定的规则下,他们可以相互转换,得到性质完全相同的透视变换。至于这个规则,这里就不给出了。

    转回正题,有了上面的讨论,我们就可以展开我们的变换表达式如下,

    x’’ = x * scrz * ax + z * Cx

    y’’ = x * scrz * ay + z * Cy

    z’’ = z * (Zfar*Zmax–Znear*Zmin)/(Zfar – Znear)

    –Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)

    w’’ = z

    z分量好像还有点不一样,注意到一般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*Zmin–Znear*Zmax)/(Zfar – Znear )

    + Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear)

    好了,用矩阵把这个变换写出来,

    scrz*ax 0 0 0

    0 scrz*ay 0 0

    Cx Cy (Zfar*Zmin–Znear*Zmax)/(Zfar – Znear ) 1

    0 0 Zfar*Znear*(Zmax-Zmin)/(Zfar-Znear) 0,

    这下就完全一样了。下面的任务就是看看这个变换的性质。因为最后同样要除以z,所以x,y分量上的情形的和原来我们推导的DirectX的投影变换是一样的,区别在z分量上。来看新的f(z)函数,它的图像为

    (图6)

    5.结论

    至此,我们已经完成了预定的目标。但是,将坐标变换完全掌握之后,为了做一个像样的图形程序,我们还有更多事情要做——至少在PS2上是这样。

  • 相关阅读:
    SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因
    微软BI 之SSIS 系列
    微软BI 之SSIS 系列
    微软BI 之SSIS 系列
    微软BI 之SSIS 系列
    微软BI 之SSIS 系列
    微软BI 之SSAS 系列
    微软BI 之SSRS 系列
    微软BI 之SSRS 系列
    配置 SQL Server Email 发送以及 Job 的 Notification通知功能
  • 原文地址:https://www.cnblogs.com/qingsunny/p/2919788.html
Copyright © 2011-2022 走看看