视锥体剔除是Unity常用的剔除方法,其原理就是通过判定目标包围盒与组成相机视锥体的6个平面进行同侧判定,只要在6个平面之间的包围盒即为可见。
具体原理可见参考以下文章:
不过这个文档里面主要讲原理,没有Unity的具体实现。本文根据其原理,给出一个视锥体裁剪的剔除算法的实现。另:当初做这个剔除是为了在 ECS 里面使用的,因此这里的写法都是 ECS 的实现,但是原理是一样的。
1、获取相机平面
所谓相机的裁剪视锥体,如下图所示:
其实就是单纯地6个平面,在Unity中调用对应API可以很方便地获取到:
GeometryUtility.CalculateFrustumPlanes(Camera camera, Plane[] planes)
Unity的原生API是返回一个数组,在ECS里面是不能直接使用的。如有需要,则要提前转换成一个 NativeArray , 用一个 float4 即可缓存每个平面的法线平面方程:
private NativeArray<float4> m_FrustumPlanes;//给Ecs用的裁剪面
private Plane[] CameraSourcePlanes = new Plane[6];//原生获得的裁剪面
private Camera camera//主相机,需要外部传入
private void UpdateFrustumPlanes()
{
//通过Unity原生API来获取相机裁剪面
GeometryUtility.CalculateFrustumPlanes(camera, CameraSourcePlanes);
//这里因为要给ECS用,所以需要转换为Native数据保存。
for (int i = 0; i < 6; i++)
{
var plane = CameraSourcePlanes [i];
//保存一个平面方程
m_FrustumPlanes[i] = new float4(plane.normal, plane.distance);
}
}
这里只是准备步骤。
2、视锥体剔除算法
视锥体剔除的结果,只有三种:
public enum InsideResult//视锥体检测的结果
{
Out,//外侧
In,//包含在内(指整个包围盒都在相机视锥体内)
Partial//部分包含
};
剔除算法直接上代码了, 原理在上文的连接里面就有:
/// <summary>
/// 球状剔除
/// </summary>
/// <param name="center">球中心</param>
/// <param name="radius">半径</param>
public InsideResult Inside(float3 center, float radius)
{
int length = mPlanes.Length;
bool all_in = true;
for (int i = 0; i < length; i++)
{
float4 plane = mPlanes[i];
float3 normal = plane.xyz;
var distance = math.dot(normal, center) + plane.w;
if (distance < -radius)
return InsideResult.Out;
all_in = all_in && (distance > radius);
}
return all_in ? InsideResult.In : InsideResult.Partial;
}
球状剔除就是与文章了里的算法对应的,这里的 mPlanes 就是之前的缓存的视锥体法线平面。
这里还是简单说一下原理:将包围盒中心对裁剪面的法线平面投影,点乘的结果就是中心到平面的距离。这个距离有正有负,为正数自然是在平面同侧,为负数则在平面背侧。当然,包围盒是有半径一说,所以求出的距离还需要与半径进行比较,来判定是否整个包围盒都在平面的一侧。
这里再提供一个方形包围盒的剔除:
/// <summary>
/// 方形包围盒剔除
/// </summary>
/// <param name="center">盒子中心</param>
/// <param name="extents">外延尺寸(size的一半)</param>
public InsideResult Inside(float3 center, float3 extents)
{
int length = mPlanes.Length;
bool all_in = true;
for (int i = 0; i < length; i++)
{
float4 plane = mPlanes[i];
float3 normal = plane.xyz;
float dist = math.dot(normal, center) + plane.w;
float radius = math.dot(extents, math.abs(normal));
if (dist <= -radius)
return InsideResult.Out;
all_in &= dist > radius;
}
return all_in ? InsideResult.In : InsideResult.Partial;
}
这里唯一的区别就是半径的算法不同,盒子是将其 extern 在对应法线平面上进行投影当做半径,当然也可以理解为立方体顶点到中心的向量在法线平面上的投影。这里的算法中使用的 center 和 extents 可以直接从 BoxCollider 中把数据抄过来。
结语
只要理解了原理,算法相对还是比较简单,性能也还可以,大家可以根据自己的需求进行改造。如果不需要在ECS里面用,或者不需要自己实现的话,直接调用Unity的API也是可以的:
GeometryUtility.CalculateFrustumPlanes(mainCamera, mTempCameraPlanes);
更多Unity的相机视野判定,可以参考下文: