【Overload游戏引擎分析】从视图投影矩阵提取视锥体及overload对视锥体的封装

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+C2 Ax+A2+B2+C2 By+A2+B2+C2 Cz+A2+B2+C2 D=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的符号可以判断点的相对位置:

  • D < 0, 点位于反半空间
  • D = 0, 点位于平面上
  • D > 0, 点位于正半空间

这种特性可用于判断点是否在视锥体内部。

二、OpenGL视锥体

视锥体是摄像机能看到的区域,只有在视锥体内的物体才能被看到。其由近平面、远平面与周围四个面组成,形成一个平截头体区域。

三、Overload对视锥体的封装

Overload对视锥体的封装在文件Frustum.h、Frustum.cpp中。先看其定义:

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个平面的方程参数,为了提升操作便利性,其定义了两个枚举作为索引:

cpp 复制代码
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中,我们先看最基础的判断点是否在视锥体内:

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

其本身的代码没啥好说的,无非就是公式的翻译。

相关推荐
charlee441 小时前
Unity在安卓端如何调试输出信息
android·unity·adb·游戏引擎·真机调试
TCW11212 小时前
Minetest游戏引擎源代码解析
游戏引擎
一锅炖出任易仙21 小时前
创梦汤锅学习日记day32
学习·ai·游戏引擎
mxwin1 天前
Unity URP 法线贴图如何生成 用什么工具创建
unity·游戏引擎·贴图
mxwin2 天前
Unity URP 法线贴图色彩空间、编码与解码
unity·游戏引擎·贴图·shader
玖玥拾2 天前
Cocos学习笔记:项目框架搭建与异步加载进度
游戏引擎·cocos2d
mxwin2 天前
Unity Shader URP:将法线可视化,便于调试
unity·游戏引擎·shader
蓝黑墨水2 天前
unity相关链接
unity·游戏引擎
mxwin2 天前
Unity Shader 法线贴图的七种错误用法
unity·游戏引擎·贴图·shader
mxwin2 天前
Unity URP 切线空间详解
unity·游戏引擎·shader