讲讲不怎么有用却很有意义的包围体测试

时间:2020-12-03 11:17:01

大多游戏程序员和图形程序都知道渲染流水线这个概念,它的本质是将3D的场景映射到显示屏上的一系列操作。它主要分3个阶段:应用程序阶段,几何阶段,光栅化阶段。将摄像机位置,光照,模型的图元输入到几何阶段便是应用程序阶段。进行多边形和顶点操作把3d数据映射到2d的阶段便是几何阶段。给定进过变换和投影之后的顶点,颜色,纹理坐标,给每个像素正确配色,这个阶段叫光栅化阶段。具体的流水线概念,这篇文章不做详细介绍,这篇文章主要讲解几何阶段中的包围体测试。

几何阶段简介

几何阶段分几个阶段,流水线上按顺序为顶点数据等执行以下操作

  1. 模型视图变换:模型空间变换到观察空间
  2. 顶点着色:光照处理
  3. 投影:将模型变换到一个单位立方体内
  4. 裁剪:裁剪不显示的部分
  5. 屏幕映射:映射到屏幕坐标系

而物体剔除和背面消隐是处于模型视图变换的可选操作。

包围体测试的价值

在模型视图变换阶段时需要将物体坐标系的点先转换到世界坐标系,然后再由世界坐标系转换到视图坐标系(相机坐标系)

然而执行世界坐标到相机坐标前,需要进行几个测试。来判断物体对相机是否可见,避免场景过大时造成大量多余的计算,然后准确无误地渲染物体。而这些测试被称为隐藏面消除。

执行的测试有2种,一种是背面消隐,一种是包围体测试。

背面消隐的原理可以看这篇博客,我这篇就主要讲很少被提及的包围体测试的原理

讲原理了

原理很简单:就是用一个球体把物体包起来,然后判断球体是否在视景体外,是则丢弃这个物体。‘

半径的获得

现在我们假设有一个物体,包含一组顶点,要找到离中心远最远的顶点,最远顶点到中心点的距离便是球体的半径,代码实现如下

class GameObject
    {
        public float max_radius = 0;//最大半径
        public Mesh mesh;//网格
        public Vector3 position = Vector3.zero;//坐标
        public Vector3 rotation = Vector3.zero;
        public Matrix4x4 ObjectToWorldMatrix;//模型-世界矩阵
        public GameObject(Mesh mesh,Vector3 position)
        {
            this.mesh = mesh;
            this.position = position;
            CalculateMaxRadius();
        }
        /// <summary>
        /// 计算半径
        /// </summary>
        private void CalculateMaxRadius()
        {
            int size = mesh.Vertices.Length;
            for (int i = 0; i < size; ++i)
            {
                //计算物体包围球的最大半径
                max_radius = System.Math.Max(max_radius,
                    Vector3.DistanceSquare(position, mesh.Vertices[i].m_vertex.position));
            }
            max_radius = (float)System.Math.Sqrt(max_radius);
        }


    }

判断球体是否在视景体外

为了便于理解,我用画图工具画了个视景体的图。视景体就是涂画的浅蓝色部门区域。

讲讲不怎么有用却很有意义的包围体测试

不过为了计算,程序里的视景体是个由远裁剪面,近裁剪面,y = ymax(视景体内点y坐标的最大值),y = ymin,x = xmax,x = xmin六个面构成的长方体,我们只需要判断球体是否在长方体内即可。也就是判断球体的6个临界顶点是否在视景体内即可。

远近裁剪面

首先我们判断球体是否在远近裁剪面间

只需要判断球体z坐标最大的点的z坐标值是否在比近裁面的z值小,球体z坐标最小的点的z坐标值是否比远裁面的z值大即可。代码实现如下

//远近裁剪面裁剪
if (SphereCenterPos.z - go.max_radius> camera.zf||
    SphereCenterPos.z + go.max_radius < camera.zn)
{
    return go.mesh.CullFlag = true;
}

左右裁剪面和上下裁剪面

接下来是对左右裁剪面和上下裁剪面的判断,不过由于需要计算xmin,ymin,xmax,ymax,我们需要先获得摄像头的焦距

焦距就是位于视景体内的黄线长度,也就是视景体的z深度

焦距的计算和摄像头数据结构定义代码如下

class Camera
    {
        /// <summary>
        /// 观察角,弧度
        /// </summary>
        public float fov;
        /// <summary>
        /// 宽纵比
        /// </summary>
        public float aspect;
        /// <summary>
        /// 近裁平面
        /// </summary>
        public float zn;
        /// <summary>
        /// 远裁平面
        /// </summary>
        public float zf;
 
        /// <summary>
        /// 屏幕宽度
        /// </summary>
        public int ScreenHeight;
 
        /// <summary>
        /// 世界-视图 4x4矩阵
        /// </summary>
        public Matrix4x4 WorldToViewMatrix;
        /// <summary>
        /// 视图-投影 4x4矩阵
        /// </summary>
        public Matrix4x4 ViewToProjectionMatrix;
 
        public Vector3 pos;
        public Vector3 lookAt;
        public Vector3 up;
 
        /// <summary>
        /// 焦距
        /// </summary>
        public float FocalLength
        {
            get
            {
                return (float)(1f/ System.Math.Tan(fov * 0.5f) * ScreenHeight/2);
            }
        }
 
 
    }

通过代码我们可以看到,焦距的计算,只是简单的三角函数的变换,不做详细介绍,不理解可以结合下图来理解

讲讲不怎么有用却很有意义的包围体测试

完整代码

/// <summary>
        /// 物体剔除-包围球测试
        /// </summary>
        private bool CullObject(GameObject go,Vector3 SphereCenterPos)
        {
            if (go.mesh.CullFlag)
                return true;
            Camera camera = Rendering_pipeline.MainCamera;
 
            //远近裁剪面裁剪
            if (SphereCenterPos.z - go.max_radius> camera.zf||
               SphereCenterPos.z + go.max_radius < camera.zn)
            {
                return go.mesh.CullFlag = true;
            }
 
            float FocalLength = camera.FocalLength;//获得焦距
 
            //左右裁剪面剔除
            float z_test = 0.5f * camera.aspect * camera.ScreenHeight *
                SphereCenterPos.z / FocalLength;
            if (SphereCenterPos.x - go.max_radius > z_test ||
               SphereCenterPos.x + go.max_radius < -z_test)
            {
                return go.mesh.CullFlag = true;
            }
 
            //上下裁剪面剔除
            z_test = 0.5f * camera.ScreenHeight *
                SphereCenterPos.z / FocalLength;
            if (SphereCenterPos.y - go.max_radius > z_test ||
              SphereCenterPos.y + go.max_radius < -z_test)
            {
                return go.mesh.CullFlag = true;
            }
 
            return go.mesh.CullFlag = false;
        }

包围体测试的局限性

程序中的视景体实际上并没有成功代表整个视景体,包围球也不一定很好地代表整个物体。而且存在物体部分位于视景体的情况,包围体测试不一定管用。但这不代表包围体测试没有意义,至少在游戏中,能有效避免对不可见的大型区域进行处理。