overoad代码中包含一段有意思的代码,可以从视图投影矩阵逆推出摄像机的视锥体,本文来分析一下原理
视锥体是用平面来表示的,所以先看看平面的数学表达。
平面方程可以由其法线N=(A, B, C)和一个点Q=(x0,y0,z0)定义,其形式为:
A
(
x
−
x
0
)
+
B
(
y
−
y
0
)
+
C
(
z
−
z
0
)
=
0
A(x-x_{0})+B(y-y_{0})+C(z-z_{0})=0
A(x−x0)+B(y−y0)+C(z−z0)=0 整理变为:
A
x
+
B
y
+
C
z
+
D
=
0
Ax+By+Cz+D=0
Ax+By+Cz+D=0, 其中
D
=
−
A
x
0
−
B
y
0
−
C
z
0
D=−Ax_{0}−By_{0}−Cz_{0}
D=−Ax0−By0−Cz0
方程进一步可以将方程归一化:
A
A
2
+
B
2
+
C
2
x
+
B
A
2
+
B
2
+
C
2
y
+
C
A
2
+
B
2
+
C
2
z
+
D
A
2
+
B
2
+
C
2
=
0
\frac{A}{\sqrt{A^{2}+B^{2}+C^{2} } } x + \frac{B}{\sqrt{A^{2}+B^{2}+C^{2} } }y+\frac{C}{\sqrt{A^{2}+B^{2}+C^{2} } }z+\frac{D}{\sqrt{A^{2}+B^{2}+C^{2} } } = 0
A2+B2+C2Ax+A2+B2+C2By+A2+B2+C2Cz+A2+B2+C2D=0 写成通用格式
a
x
+
b
y
+
c
z
+
d
=
0
ax+by+cz+d=0
ax+by+cz+d=0
那么点
p
=
(
x
1
,
y
1
,
z
1
)
p=(x_{1}, y_{1}, z_{1})
p=(x1,y1,z1)到平面的距离为:
D
=
a
x
1
+
b
y
1
+
c
z
1
+
d
D=ax_{1}+by_{1}+cz_{1}+d
D=ax1+by1+cz1+d
一个平面会将空间分成两个半空间(halfspace),进一步法线的朝向的空间称为正半空间(positive halfspace),法线背离的空间称为反半空间(negative halfspace)。根据D的符号可以判断点的相对位置:
这种特性可用于判断点是否在视锥体内部。
视锥体是摄像机能看到的区域,只有在视锥体内的物体才能被看到。其由近平面near、远平面far与周围四个面top、bottom、left、right组成,形成一个平截头体区域。
不过在构建视锥体时一般不直接输入6个平面的方程,常用另外一组更直观易懂的参数:
具体含义建下图:
Overload对视锥体的封装在文件Frustum.h、Frustum.cpp中。先看其定义:
class Frustum
{
public:
/**
* 根据视图投影矩阵提取视锥体
* @param p_viewProjection
*/
void CalculateFrustum(const OvMaths::FMatrix4& _viewProjection);
/**
* 判断点是不是在视锥体内
* @param p_x
* @param p_y
* @param p_z
*/
bool PointInFrustum(float p_x, float p_y, float _z) const;
/**
* 判断球是不是在视锥体内
* @param p_x
* @param p_y
* @param p_z
* @param p_radius
*/
bool SphereInFrustum(float p_x, float p_y, loat p_z, float p_radius) const;
/**
* 判断立方体是不是在视锥体内
* @param p_x
* @param p_y
* @param p_z
* @param p_size
*/
bool CubeInFrustum(float p_x, float p_y, float _z, float p_size) const;
/**
* 判断包围球是不是在视锥体内
* @param p_boundingSphere
* @param p_transform
*/
bool BoundingSphereInFrustum(const vRendering::Geometry::BoundingSphere& _boundingSphere, const OvMaths::FTransform& _transform) const;
/**
* 返回近平面
*/
std::array<float, 4> GetNearPlane() const;
/**
* 返回远平面
*/
std::array<float, 4> GetFarPlane() const;
private:
float m_frustum[6][4]; // 6个平面的方程参数
};
m_frustum保存着6个平面的方程参数,为了提升操作便利性,其定义了两个枚举作为索引:
enum FrustumSide
{
RIGHT = 0, // The RIGHT side of the frustum
LEFT = 1, // The LEFT side of the frustum
BOTTOM = 2, // The BOTTOM side of the frustum
TOP = 3, // The TOP side of the frustum
BACK = 4, // The BACK side of the frustum
FRONT = 5 // The FRONT side of the frustum
};
// 平面方程的参数索引
enum PlaneData
{
A = 0, // The X value of the plane's normal
B = 1, // The Y value of the plane's normal
C = 2, // The Z value of the plane's normal
D = 3 // The distance the plane is from the origin
};
函数的具体实现在文件Frustum.cpp中,我们先看最基础的判断点是否在视锥体内:
bool OvRendering::Data::Frustum::PointInFrustum(float x, float y, float z) const
{
for (int i = 0; i < 6; i++)
{
if (m_frustum[i][A] * x + m_frustum[i][B] * y + m_frustum[i][C] * z + m_frustum[i][D] <= 0)
{
return false;
}
}
return true;
}
定义视锥体的面法线都是朝外的,如果点在视锥体内,点到6个面的距离必须全部小于0。进一步判断球体是否完全在视锥体内,距离必须小于半径的负数。
最后分析一下CalculateFrustum,它是根据一个视图投影矩阵反向构建一个视锥体,具体公式怎么来的可以参考这篇文章,里面将的特别详细:
Fast Extraction of Viewing Frustum Planes from the World View-Projection Matrix
其本身的代码没啥好说的,无非就是公式的翻译。