【Unity】相机视锥体剔除算法

发布于:2022-12-06 ⋅ 阅读:(936) ⋅ 点赞:(0)

        视锥体剔除是Unity常用的剔除方法,其原理就是通过判定目标包围盒与组成相机视锥体的6个平面进行同侧判定,只要在6个平面之间的包围盒即为可见。

        具体原理可见参考以下文章:

        视锥体剔除(Frustum Culling)算法详解

        不过这个文档里面主要讲原理,没有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的相机视野判定,可以参考下文:

        【Unity】判断 物体/坐标 是否在相机范围内

本文含有隐藏内容,请 开通VIP 后查看