• 【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(xx0)+B(yy0)+C(zz0)=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=Ax0By0Cz0
             方程进一步可以将方程归一化:
    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视锥体

    视锥体是摄像机能看到的区域,只有在视锥体内的物体才能被看到。其由近平面near、远平面far与周围四个面top、bottom、left、right组成,形成一个平截头体区域。
    在这里插入图片描述
    不过在构建视锥体时一般不直接输入6个平面的方程,常用另外一组更直观易懂的参数:

    1. fov – 视锥体的垂直张角
    2. Near – 视锥体的近平面距离
    3. Far – 视锥体的远平面距离
    4. aspect – 相机视口的长宽比

    具体含义建下图:
    在这里插入图片描述

    三、Overload对视锥体的封装

    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个平面的方程参数
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    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
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    函数的具体实现在文件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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    定义视锥体的面法线都是朝外的,如果点在视锥体内,点到6个面的距离必须全部小于0。进一步判断球体是否完全在视锥体内,距离必须小于半径的负数。
    最后分析一下CalculateFrustum,它是根据一个视图投影矩阵反向构建一个视锥体,具体公式怎么来的可以参考这篇文章,里面将的特别详细:
    Fast Extraction of Viewing Frustum Planes from the World View-Projection Matrix
      其本身的代码没啥好说的,无非就是公式的翻译。

  • 相关阅读:
    Python 完美诠释"高内聚"概念的 IO 流 API 体系结构
    【Python】Python中的lambda表达式
    Netty入门——组件(EventLoop)
    Vue3 源码阅读(9):渲染器 —— diff 算法
    【单目3D目标检测】项目实战-道路车辆/行人3D目标检测
    CSS笔记——BFC(块级格式化上下文)
    【软考软件评测师】第三十章 操作系统(PV操作与死锁)
    软件开发项目文档系列之五如何撰写需求规格说明书
    【JavaEE初阶系列】——HTTP协议
    IO系列第三篇——NIO(Selector)
  • 原文地址:https://blog.csdn.net/loveoobaby/article/details/133657578