再看irrlicht的数学库中matrix4实现时,对这个透视投影变换矩阵的公式十分疑惑,经过艰苦的奋斗终于搞清楚是怎么一回事在这里和大家分享一下
irrlicht中使用的透视矩阵是DirectX风格的,但个人偏好于OpenGL的风格所以下面的实现将会使用OpenGL的风格实现。
注意:这里使用的是水平视角和Y:X,DirectX中使用的是垂直视角和X:Y
inline void SetPerspectiveFovMatrixLH( f32 fieldOfViewRadians, f32 aspectRatio, f32 zNear, f32 zFar) { Assert( Equal( aspectRatio, 0.f ) ); //divide by zero Assert( Equal( fieldOfViewRadians, 0.f ) ); //divide by zero Assert( Equal( zNear, zFar ) ); //divide by zero f32 wc = 1 / tan( fieldOfViewRadians / 2 ); f32 hc = wc / aspectRatio; m_[0][0] = wc; m_[0][1] = 0; m_[0][2] = 0; m_[0][3] = 0; m_[1][0] = 0; m_[1][1] = hc; m_[1][2] = 0; m_[1][3] = 0; m_[2][0]= 0; m_[2][1]= 0; m_[2][2] = ( zFar + zNear ) / ( zFar - zNear ); m_[2][3] = 1.0f; m_[3][0] = 0; m_[3][1] = 0; m_[3][2] = 2 * zNear * zFar / ( zNear - zFar ); m_[3][3] = 0; }
这里是我写的透视矩阵的一段代码,得到的矩阵式这样子的:
w 0 0 0
0 h 0 0
0 0 (f+n)/(f-n) 1
0 0 2nf/(n-f) 0
实际上这个叫透视矩阵的东西实现的不仅仅是透视功能,如果要实现透视功能实际上的矩阵式这样子的:
那多出来的这部分:
是用来干什么的呢,实际上”透视矩阵“的功能不仅仅是透视,还有一个裁剪的功能,它可以把不在视景体内的顶点裁剪掉,则多出来的部分就是用来进行裁剪的。
这部分裁剪矩阵主要是使裁剪空间的规范化。
以顶点(x,y,z)为例,我们可以总结出一套裁剪的原则。
当z小于zNear时被裁剪,z大于zFar时被裁剪,x小于-z*tan(水平视角/2)时被裁剪,x大于z*tan(水平视角/2)时被裁剪,y小于-z*tan(垂直视角Fov/2)时被裁剪,y大于z*tan(垂直视角/2)时被裁剪。
但这个原则有些复杂,根据《3D数学基础:图形与游戏开发》书上所讲
PS:这里的w指顶点尚未转换时的z值,而x,y,z是指经过裁剪矩阵转换后的值
但要如何规范化这六个裁剪面,使其有这个简单的形式呢,这就是很多书上都写得不明不白的地方了,答案是把当前的视景体转换成水平视角和垂直视角都为90度的视景体里面,再对近裁剪面和远裁剪面做出一些调整。
我们可以看到tan(90/2)=tan(45)=1,即x小于-z时被裁剪,x大于z时被裁剪,y小于-z时被裁剪,y大于z时被裁剪。
但要如何做出转换呢,当前水平视角为fieldOfViewRadians,水平方向上我们知道x的最大为z * tan( fieldOfViewRadians / 2 )(此z为未经裁剪矩阵转换过的z),当前水平视角为90度,水平方向上我们知道x的最大为w(w指顶点尚未转换时的z值),则水平方向上的转换比例为
wc = w / ( z * tan( fieldOfViewRadians / 2 ) )= 1 / tan( fieldOfViewRadians / 2 );
又因为我们知道了视平面的长宽比,可以求出当前水平视角为fieldOfViewRadians,垂直方向上我们知道x的最大为z * tan( fieldOfViewRadians / 2 ) * aspectRatio
hc = w / ( z * tan( fieldOfViewRadians / 2 ) * aspectRatio ) = wc / aspectRatio;
最后是近裁剪面和远裁剪面的问题,我们可以得出 -z <= z * u + v <= z (此z为未经裁剪矩阵转换过的z)
即 zNear * u + v = -zNear;
zFar * u + v = zFar;
得 u = (zFar + zNear) / (zFar - zNear);
v = -zNear - zNear * u = 2 * zNear * zFar / (zNear - zFar)
即得裁剪矩阵为
wc 0 0 0
0 hc 0 0
0 0 u 0
0 0 v 0
把裁剪矩阵和真正的透视矩阵结合起来就变成了”透视矩阵“
还有一种透视投影的方式是这样子的
// widthOfViewVolume,heightOfViewVolume:近裁剪面的长宽 inline void SetPerspectiveMatrixLH( f32 widthOfViewVolume, f32 heightOfViewVolume, f32 zNear, f32 zFar) { Assert( Equal( widthOfViewVolume, 0.f ) ); //divide by zero Assert( Equal( heightOfViewVolume, 0.f ) ); //divide by zero Assert( Equal( zNear, zFar ) ); //divide by zero m_[0][0]= 2 * zNear / widthOfViewVolume ; m_[0][1]= 0; m_[0][2]= 0; m_[0][3]= 0; m_[1][0]= 0; m_[1][1]= 2 * zNear / heightOfViewVolume; m_[1][2]= 0; m_[1][3]= 0; m_[2][0]= 0; m_[2][1]= 0; m_[2][2] = ( zFar + zNear ) / ( zFar - zNear ); m_[2][3] = 1.0f; m_[3][0] = 0; m_[3][1] = 0; m_[3][2] = 2 * zNear * zFar / ( zNear - zFar ); m_[3][3] = 0; }
这里不过是做了一些小变换
tan( Fov / 2 ) = 对边 / 邻边 = ( widthOfViewVolume / 2 ) / zNear;
1 / tan( Fov / 2 ) = zNear * 2 / widthOfViewVolume;