关于本文html
本文主要讲解从数学的角度如何推导出Stage3D中用到的两个投影矩阵编程
perspectiveLH缓存
public function perspectiveLH(width:Number,height:Number,zNear:Number,zFar:Number):void { this.copyRawDataFrom(Vector.<Number>([ 2.0 * zNear / width, 0.0, 0.0, 0.0, 0.0, 2.0 * zNear / height, 0.0, 0.0, 0.0, 0.0, zFar / (zFar - zNear), 1.0, 0.0, 0.0, zNear * zFar / (zNear - zFar), 0.0 ])); }
perspectiveFieldOfViewLHide
public function perspectiveFieldOfViewLH(fieldOfViewY:Number,aspectRatio:Number,zNear:Number,zFar:Number):void { var yScale:Number = 1.0 / Math.tan(fieldOfViewY / 2.0); var xScale:Number = yScale / aspectRatio; this.copyRawDataFrom(Vector.<Number>([ xScale, 0.0, 0.0, 0.0, 0.0, yScale, 0.0, 0.0, 0.0, 0.0, zFar / (zFar - zNear), 1.0, 0.0, 0.0, (zNear * zFar) / (zNear - zFar), 0.0 ])); }
参考资料函数
在此感谢Twinsen的帮助和点化 呵呵测试
关于线性插值的解释this
Twinsen文章中已经给出了线性插值的公式,包括Wiki里面也给出了推导。以前对这个公式很困惑,感受似曾相识却又不是很好理解。当彻底搞透矩阵转换后,返回来再看线性插值,发现这彻底就是初中时候学的直线方程嘛!spa
线性插值的描述:.net
给一个x属于[a, b],找到y属于[c, d],使得x与a的距离比上ab长度所获得的比例,等于y与c的距离比上cd长度所获得的比例,用数学表达式描述很容易理解:3d
这样,从a到b的每个点都与c到d上的惟一一个点对应。有一个x,就能够求得一个y。
此外,若是x不在[a, b]内,好比x < a或者x > b,则获得的y也是符合y < c或者y > d,比例仍然不变,插值一样适用。
这个是直接复制Twinsen上面的原话,如今让咱们换个角度考虑这句话,看Wiki上面这张图
其中x0,x1就是对应的a,b. y0,y1就是对应的c,d。换成咱们比较熟悉的表述方法就是,已知直线上的两点求该直线方程。这样你们就明白应该怎么作了吧
线性插值的做用
在矩阵推导过程当中线性插值有何用处?其实线性插值解决的就是一个压缩坐标点区域的做用,好比p点属于区域A(0,600),但愿将其压缩成区域B(0,400).使得在区域B中的p’和区域A中的p存在一一对应的关系。
解决方式就是画图,注意横纵坐标的名称,分别为压缩前的区域和压缩后的区域。而那条红线的数学表示方法也就是线性插值
二维平面上面的投影
首先让咱们抛开z值,先推导x,y方向上面的投影 (具体的推导过程请参考Twinsen的文章)
x和y两个方向的最终投影为
换句话表述就是,根据公式 x'=N*(x/z) 也就能够获得世界坐标系中的x点在投影平面上面的x’点。
关于CCV(Canonical View Volume)和NDC(Normalized Device Coordinates)
在Adobe的Working with Stage3D and perspective projection文章中,介绍过NDC的概念。文章在介绍该概念的那段最后一句话提到
Stage3D and the GPU use the data(此时data指的是NDC后的顶点数据) from the output of your Shader in clip space form to carry on internally with the perspective divide.
也就是在后续的渲染管道中Stage3D和GPU都认为你已经经过矩阵转化将x,y坐标(目前先不讨论z),转化到了(-1,1)这个范围内。
回头再来看以前推导出的x'点(x'=N*(x/z)),该点的区域在没压缩前是投影面的(left,right),
而咱们须要作的也就是将这个区域A(left,right)压缩到区域(-1,1)
具体的数学推导能够看Twinsen的文章,经过推导得出特殊形式下(投影平面的中心和x-y平面的中心重合) 新的x,y坐标方程为
此时 right-left 也就是width ,top-bottom 就是height.
再来看perspectiveLH中前两行里面不为0的值,就是推导公式中的结果,其中Twinsen公式中的N对应的是perspectiveLH公式中的zNear
public function perspectiveLH(width:Number,height:Number,zNear:Number,zFar:Number):void { this.copyRawDataFrom(Vector.<Number>([ 2.0 * zNear / width, 0.0, 0.0, 0.0, 0.0, 2.0 * zNear / height, 0.0, 0.0, 0.0, 0.0, zFar / (zFar - zNear), 1.0, 0.0, 0.0, zNear * zFar / (zNear - zFar), 0.0 ])); }
注意投影平面的left,right可能为任何值,不必定非要为-1,1。只有在压缩后新的x'值的取值范围才从left,right压缩到了(-1,1)
关于width 和 height 取值的进一步说明
由于Shader最后须要将点范围缩至(-1,1)这个范围,因此perspectiveLH中的width参数也就必定为1-(-1)=2,可是height参数为2么?这个就要看你屏幕区域是否为正方形。
context3D.configureBackBuffer(500, 500, 1, true); //方形区域 context3D.configureBackBuffer(800, 600, 1, true); //普屏4:3 context3D.configureBackBuffer(1440, 900, 1, true); //宽屏16:9
在Stage3D被使用时候都须要设置背景缓冲区大小,这个函数同时也就设置了宽高比.
我之前错误的觉得Stage3D会很智能,在configureBackBuffer函数中设置好宽高比后,代码中用到的投影函数都应该把x,y方向转化成(-1,1)范围内。后续模块处理完这些数据时候,光栅化时候自动根据宽高比显示在屏幕上。
后来作了一个小Demo测试发现,Stage3D并无处理这件事情。也就是须要使用者本身处理这个问题。
因此,若是你选择使用perspectiveLH做为投影矩阵,configureBackBuffer时候选择的宽高比是4:3(不用关心具体的像素值,只关心比例),
那么真正的height区域就应该是(-1/aspectRatio,1/aspectRatio). 其中aspectRatio=4/3
关于Z值
当理解了x,y如何转换为x'和y'而且完成了线性插值,如今可让咱们来看看关于z值的问题了, 这个问题我琢磨了好久,可是到目前为止仍是有些疑惑。
整体来讲,对于透视投影这件事,实际上是不须要z值。由于最终已经投影到了一个平面上面,一个平面只关心x,y两个坐标。对于z值,实际上是可有可无的。那什么地方须要用到?
1·排序
2·纹理映射
关于这些又是另外两个比较大块的区域,我查阅了一些资料但理解的仍是不太透彻。能够得出的大概思想是
在世界坐标系中可使用线性插值,可是因为作了投影计算,若是对变换后的点z'(假设就是直接复制z值的变换),使用线性插值其结果是错误的
能够认为图中蓝色区域的线段为投影后的区域,红色为投影前,蓝色区域每一小块是相等的,可是红色区对应却不相等。
这个问题同时会影响排序,纹理映射,因此要选择1/z 而不是直接使用z值。但使用1/z 并非直接把原来的z值变成1/z而是
要变成z= a(1/z’)+b 这种形式,也就是通常的直线方程形式。
关于为什么要使用1/z 而不是使用z值 能够参考Twinsen的另外两篇文章 深刻探索透视纹理映射(上,下),还有 《3D游戏大师编程技巧》书中的第11章 深度缓存和可见性。
关于第二个问题,为什么不直接使用1/z 而要用 a(1/z')+b的形式,我仍是没太明白,因此就不作共多介绍了,若是有人比较懂的话,麻烦告诉我一声 谢谢。
若是能够理解为什么将z变成 a(1/z')+b这件事以后,后续的数学推论Twinsen的文章中已经写得很是清楚了,大致思想就是同x,y同样。将变换后的新z值也线性插值到(-1,1) 或者(0,1) 空间内
如何理解perspectiveFieldOfViewLH里面的矩阵
perspectiveFieldOfViewLH和perspectiveLH实际上是同一个东西,要理解这个问题咱们还要看一下以下两张图
首先先看第一张图,如何肯定p'的坐标,实际上是看是看np平面的位置,和其余变量没有任何关系。也就是np平面越靠近fp平面,p'点的位置就越往外。
或者换句话说p'点的x值越大,显示在平面上则该图形最大。
另外还有一个已知条件就是np平面的宽度为1-(-1)=2.
而后再让咱们来看第二张图,黄线和绿线对应的就是np平面。一种方式是给定zNear值,也就是直接指定该平面距离原点的偏移值。
另一种方式是指定角度FOV(Field Of View)也就是指定黑色或者蓝色那个角度有多少
而后 根据三角函数能够获得 1/zNear= Math.tan(fieldOfViewY / 2.0);
其中 1/zNear中的1 是由于黄线或者绿线的总宽度为2,一半就是1。fieldOfViewY/2 就是一半的角度.
此时让咱们回过头再来看perspectiveFieldOfViewLH中的矩阵
public function perspectiveFieldOfViewLH(fieldOfViewY:Number,aspectRatio:Number,zNear:Number,zFar:Number):void { var yScale:Number = 1.0 / Math.tan(fieldOfViewY / 2.0); var xScale:Number = yScale / aspectRatio; this.copyRawDataFrom(Vector.<Number>([ xScale, 0.0, 0.0, 0.0, 0.0, yScale, 0.0, 0.0, 0.0, 0.0, zFar / (zFar - zNear), 1.0, 0.0, 0.0, (zNear * zFar) / (zNear - zFar), 0.0 ])); }
其中yScale也就是上图解释的问题,而xScale是根据宽高比得出来的,详情能够参考 关于width 和 height 取值的进一步说明 这一小节的内容。
总结
到此我应该已经阐述清楚了perspectiveLH和perspectiveFieldOfViewLH是如何推导出来的,具体推导过程还请参考Twinsen的两篇文章。我只是换个角度阐述了一下对两个公式的理解,在此感谢Twinsen对我以前问题热心的解答,谢谢。
关于Stage3D渲染管道的一点疑惑
目前我对Stage3D的渲染管道仍是有些疑惑的,Stage3D和GPU到底作了什么又没作什么?根据我对3D渲染管道的理解,要渲染一个物体,首先须要将物体从自身的坐标系转化到世界坐标系,
而后再进行摄像头的旋转平移等操做,这些操做完成后应该作一系列测试以便剔除没有必要的图形。
好比若是一个世界上面有1000个图形,先根据可见度(BPS树?)剔除大部分物体,而后对剩余物体作AABB测试,保留那些所有或者一部分在视景体内部的物体。对剩余物体进行背面消除,而后在对部分在视口内部的物体进行3D剪裁。
将最终的顶点结果传入GPU,进行后续的处理(打光和加入纹理),最后光栅化到屏幕上。
上面那张图就是Stage3D的渲染管道示意图,我原本觉得Stage3D会很“聪明”,在Vertex Shader中的矩阵(也就是上面文章在介绍的,注:此矩阵仅包含投影,不是最终使用的矩阵),只要将全部坐标点所有转换为NDC形式的,Stage3D会作
剩下全部的操做,其中包括:
1·剔除不在屏幕上面显示的
2·自动将屏幕缩放为正常尺寸(详情见 关于width 和 height 取值的进一步说明 小节)
第二点根据测试,我发现Stage3D没有这么作,因此也就是须要编程者本身考虑这个问题。若是是这样将全部坐标缩放到NDC后其实Stage3D应该仅仅作了“渲染”这件事,至于和剔除相关的,也就是第一点,Stage3D应该没有涉及。
可是比较疑惑的是,上图中明显包含一个 “Viewport clipping”模块,这个模块的目的是进行2D剪裁么?而且Context3D中有setCulling这个函数,也就是说Stage3D能够根据设置参数进行背面消除。
这让我对Stage3D到底作了什么,没有作什么非常困惑。
可不能够这么说:若是咱们须要作一个3D引擎,那引擎部分须要涉及到的部分有
1·根据BSP树进行剔除(剔除根本不可见的物体,好比一面墙后面的物体,或者说本身视角后方的物体)
2·进行AABB测试(剔除那些不在视景体内部的物体)
3·背面消除(由Stage3D处理,引擎不须要考虑)
4·3D剪裁(Stage3D会作2D剪裁,引擎也不须要考虑。 可是那些z值小于zNear的点怎么办?)
不知道我理解的是否正确?但愿获得高手的解答,谢谢