大多游戏程序员和图形程序都知道渲染流水线这个概念,它的本质是将3D的场景映射到显示屏上的一系列操作。它主要分3个阶段:应用程序阶段,几何阶段,光栅化阶段。将摄像机位置,光照,模型的图元输入到几何阶段便是应用程序阶段。进行多边形和顶点操作把3d数据映射到2d的阶段便是几何阶段。给定进过变换和投影之后的顶点,颜色,纹理坐标,给每个像素正确配色,这个阶段叫光栅化阶段。具体的流水线概念,这篇文章不做详细介绍,这篇文章主要讲解几何阶段中的包围体测试。
几何阶段简介
几何阶段分几个阶段,流水线上按顺序为顶点数据等执行以下操作
- 模型视图变换:模型空间变换到观察空间
- 顶点着色:光照处理
- 投影:将模型变换到一个单位立方体内
- 裁剪:裁剪不显示的部分
- 屏幕映射:映射到屏幕坐标系
而物体剔除和背面消隐是处于模型视图变换的可选操作。
包围体测试的价值
在模型视图变换阶段时需要将物体坐标系的点先转换到世界坐标系,然后再由世界坐标系转换到视图坐标系(相机坐标系)
然而执行世界坐标到相机坐标前,需要进行几个测试。来判断物体对相机是否可见,避免场景过大时造成大量多余的计算,然后准确无误地渲染物体。而这些测试被称为隐藏面消除。
执行的测试有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;
}
包围体测试的局限性
程序中的视景体实际上并没有成功代表整个视景体,包围球也不一定很好地代表整个物体。而且存在物体部分位于视景体的情况,包围体测试不一定管用。但这不代表包围体测试没有意义,至少在游戏中,能有效避免对不可见的大型区域进行处理。