文章摘要
视锥体裁剪是提升渲染性能的关键技术,通过判断物体是否在摄像机视锥体内来减少无效渲染。Unity的实现分为C++底层和C#接口层:C++层负责视锥体平面计算、包围盒测试、批量裁剪和性能优化(如SIMD/多线程);C#层提供查询接口(如GeometryUtility)和扩展支持。核心裁剪逻辑在C++层完成,C#层仅作为工具封装,开发者可通过接口获取裁剪结果或实现自定义剔除。整体流程为摄像机参数→C++视锥体计算→批量裁剪→渲染队列生成→C#可选查询。
一、视锥体裁剪原理
视锥体裁剪是指:
在渲染前,判断场景中的物体是否在摄像机的可见范围(视锥体)内,只有在视锥体内的物体才会被提交到渲染管线。这样可以大幅减少无用的渲染,提高性能。
- 视锥体:由摄像机的位置、朝向、视野角、近平面、远平面等参数确定的一个六面体(六个平面)。
- 裁剪对象:通常是物体的包围盒(AABB/OBB)或包围球。
二、C++层实现(引擎底层)
1. 视锥体的数学表示
- 视锥体由6个平面(左、右、上、下、近、远)组成,每个平面用法向量和平面方程表示。
- C++层会根据摄像机的投影矩阵和视图矩阵,计算出这6个平面。
struct Plane {
Vector3 normal;
float d; // 平面方程: normal.x * x + normal.y * y + normal.z * z + d = 0
};
Plane frustumPlanes[6];
2. 包围盒与视锥体的相交测试
- 对每个物体的包围盒(AABB/OBB),依次与6个平面做测试。
- 如果包围盒的所有顶点都在某个平面的外侧,则该物体完全在视锥体外,可剔除。
- 如果包围盒与所有平面都相交或在内侧,则该物体需要渲染。
bool IsBoxInFrustum(const AABB& box, const Plane* frustumPlanes) {
for (int i = 0; i < 6; ++i) {
if (box is completely outside frustumPlanes[i])
return false; // 剔除
}
return true; // 保留
}
3. 剔除流程
- C++层遍历场景所有渲染对象,做视锥体裁剪。
- 只将通过裁剪的对象加入渲染队列。
4. SIMD/多线程优化
- C++层通常会用SIMD指令、批量处理和多线程加速大批量物体的裁剪。
三、C#层实现(脚本层/接口层)
1. 主要职责
- 提供访问和控制接口,允许开发者自定义或查询裁剪结果。
- 典型接口有:
GeometryUtility.CalculateFrustumPlanes(Camera)
GeometryUtility.TestPlanesAABB(planes, bounds)
Renderer.isVisible
(只读,反映底层裁剪结果)
2. 典型用法
// 1. 获取摄像机的视锥体平面
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
// 2. 判断某个物体的包围盒是否在视锥体内
if (GeometryUtility.TestPlanesAABB(planes, renderer.bounds)) {
// 在视锥体内
}
- 这些C#接口本质上是对C++底层的包装,实际的数学计算和批量裁剪都在C++层完成。
3. 自定义剔除
- 开发者可以用C#层的接口做自定义剔除(如LOD、分区裁剪等),但大部分情况下无需手动处理,Unity会自动完成。
四、C++层与C#层的分工
层级 | 主要职责 |
---|---|
C++ | 视锥体平面计算、包围盒测试、批量裁剪、渲染队列管理、性能优化 |
C# | 提供接口、辅助工具、允许自定义/查询裁剪结果、可做特殊逻辑扩展 |
- 核心裁剪逻辑和性能关键都在C++层,C#层只是接口和工具。
五、流程图
-
C++层
- 计算视锥体平面
- 遍历物体包围盒
- 剔除不可见物体
- 生成渲染队列
-
C#层
- 可选:调用接口获取平面/测试包围盒
- 可选:自定义剔除逻辑
六、伪代码总览
C++层
// 1. 计算视锥体平面
Plane frustumPlanes[6] = CalculateFrustumPlanes(camera);
// 2. 剔除不可见物体
for (auto& obj : sceneObjects) {
if (IsBoxInFrustum(obj.bounds, frustumPlanes)) {
renderQueue.push_back(obj);
}
}
C#层
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
if (GeometryUtility.TestPlanesAABB(planes, renderer.bounds)) {
// 物体在视锥体内
}
七、总结
- Unity视锥体裁剪的核心算法和性能优化都在C++底层实现,C#层提供接口和工具,方便开发者自定义或查询裁剪结果。
- 剔除流程:摄像机参数→C++计算视锥体→C++批量裁剪→生成渲染队列→C#可选自定义/查询。
八、具体C++源码片段、裁剪算法优化、或与其他剔除技术(如遮挡剔除、层级剔除)的对比
下面分别给出:
- 视锥体裁剪的C++源码片段(核心算法)
- 裁剪算法优化思路
- 与遮挡剔除、层级剔除的对比说明
1. 视锥体裁剪 C++ 源码片段
1.1 视锥体平面提取
通常从摄像机的投影矩阵和视图矩阵组合得到视锥体的六个平面。下面是从组合矩阵提取平面的典型代码:
struct Plane {
Vector3 normal;
float d; // 平面方程: normal.x * x + normal.y * y + normal.z * z + d = 0
// 点到平面的距离
float Distance(const Vector3& pt) const {
return normal.x * pt.x + normal.y * pt.y + normal.z * pt.z + d;
}
};
// 从视图投影矩阵提取六个平面
void ExtractFrustumPlanes(const Matrix4x4& vp, Plane planes[6]) {
// vp = projection * view
// 左
planes[0].normal.x = vp.m[3] + vp.m[0];
planes[0].normal.y = vp.m[7] + vp.m[4];
planes[0].normal.z = vp.m[11] + vp.m[8];
planes[0].d = vp.m[15] + vp.m[12];
// 右
planes[1].normal.x = vp.m[3] - vp.m[0];
planes[1].normal.y = vp.m[7] - vp.m[4];
planes[1].normal.z = vp.m[11] - vp.m[8];
planes[1].d = vp.m[15] - vp.m[12];
// 下
planes[2].normal.x = vp.m[3] + vp.m[1];
planes[2].normal.y = vp.m[7] + vp.m[5];
planes[2].normal.z = vp.m[11] + vp.m[9];
planes[2].d = vp.m[15] + vp.m[13];
// 上
planes[3].normal.x = vp.m[3] - vp.m[1];
planes[3].normal.y = vp.m[7] - vp.m[5];
planes[3].normal.z = vp.m[11] - vp.m[9];
planes[3].d = vp.m[15] - vp.m[13];
// 近
planes[4].normal.x = vp.m[3] + vp.m[2];
planes[4].normal.y = vp.m[7] + vp.m[6];
planes[4].normal.z = vp.m[11] + vp.m[10];
planes[4].d = vp.m[15] + vp.m[14];
// 远
planes[5].normal.x = vp.m[3] - vp.m[2];
planes[5].normal.y = vp.m[7] - vp.m[6];
planes[5].normal.z = vp.m[11] - vp.m[10];
planes[5].d = vp.m[15] - vp.m[14];
// 归一化
for (int i = 0; i < 6; ++i) {
float len = sqrt(planes[i].normal.x * planes[i].normal.x +
planes[i].normal.y * planes[i].normal.y +
planes[i].normal.z * planes[i].normal.z);
planes[i].normal.x /= len;
planes[i].normal.y /= len;
planes[i].normal.z /= len;
planes[i].d /= len;
}
}
1.2 AABB与视锥体平面测试
// 判断AABB是否在视锥体内
bool IsAABBInFrustum(const AABB& box, const Plane planes[6]) {
for (int i = 0; i < 6; ++i) {
// 对每个平面,找出AABB最靠近平面的顶点
Vector3 p;
p.x = (planes[i].normal.x >= 0) ? box.max.x : box.min.x;
p.y = (planes[i].normal.y >= 0) ? box.max.y : box.min.y;
p.z = (planes[i].normal.z >= 0) ? box.max.z : box.min.z;
// 如果最靠近的点在平面外,则整个AABB在平面外
if (planes[i].Distance(p) < 0)
return false;
}
return true;
}
1.3 剔除流程
Plane frustumPlanes[6];
ExtractFrustumPlanes(cameraVPMatrix, frustumPlanes);
for (auto& obj : sceneObjects) {
if (IsAABBInFrustum(obj.bounds, frustumPlanes)) {
renderQueue.push_back(obj);
}
}
2. 剔除算法优化思路
2.1 批量处理与SIMD
- 利用SIMD指令(如SSE/AVX)并行处理多个包围盒与平面测试,提升CPU利用率。
- 批量裁剪时,数据结构要对齐,便于向量化。
2.2 空间分区(如八叉树/四叉树/BVH)
- 先对场景做空间分区,裁剪时只测试分区节点的包围盒,快速排除大批物体。
- 只对分区内的物体做详细测试,减少冗余计算。
2.3 多线程
- 剔除任务可分配到多个线程并行处理,适合大场景。
2.4 包围球快速测试
- 对于远距离物体,先用包围球做粗略测试,后续再用AABB/OBB精细测试。
2.5 剔除结果缓存
- 对静态物体,帧间可缓存裁剪结果,减少重复计算。
3. 与其他剔除技术的对比
技术 | 原理 | 优点 | 缺点/局限 | 适用场景 |
---|---|---|---|---|
视锥体裁剪 | 摄像机视锥体外的物体不渲染 | 简单高效,必需 | 不能剔除被遮挡的物体 | 所有场景 |
遮挡剔除 | 被其他物体遮挡的物体不渲染 | 可进一步减少渲染 | 算法复杂,需额外数据结构 | 大型室内、城市等 |
层级剔除 | 空间分区树(如八叉树、BVH) | 批量剔除,提升效率 | 构建和维护有成本 | 大场景、静态物体多 |
3.1 遮挡剔除(Occlusion Culling)
- 典型算法如Portal Culling、Hardware Occlusion Query、Hi-Z Buffer等。
- Unity支持遮挡剔除(如Occlusion Culling),但比视锥体裁剪慢,适合复杂场景。
3.2 层级剔除(Hierarchical Culling)
- 利用空间分区树(如八叉树、BVH),先裁剪大块区域,再细分到物体。
- 剔除效率高,适合大场景。
3.3 综合流程
通常渲染流程为:
- 层级剔除(空间分区树)→
- 视锥体裁剪(摄像机六面体)→
- 遮挡剔除(可选,复杂场景)→
- 提交渲染队列
4. 总结
- 视锥体裁剪是最基础、最必要的剔除技术,C++层实现高效,C#层可辅助。
- 优化可用SIMD、空间分区、多线程等手段。
- 遮挡剔除和层级剔除可进一步提升性能,但实现复杂度更高,适合大场景。