
1. 项目概述为什么我们需要混合剔除策略在Unity里做项目尤其是开放世界、大型室内场景或者MMO这类对性能有高要求的项目性能优化是绕不开的坎。画面卡顿、帧率不稳很多时候问题就出在渲染上——CPU和GPU吭哧吭哧地渲染了一大堆玩家根本看不见的东西。这就像你明明只想看客厅的电视却把整个房子的灯都打开了浪费电不说还让电表转得飞快。这就是“剔除”技术要解决的问题。Unity提供了两种最核心的剔除机制视锥剔除和遮挡剔除。视锥剔除很简单它只渲染摄像机“视野”范围内的物体视野外的直接丢掉。这解决了“看不见就不画”的基础问题。但问题来了在视野内一个物体可能被前面的墙、山或者另一个巨大的模型完全挡住玩家同样看不见它可它依然会被提交渲染。这时候就需要遮挡剔除登场了它的任务就是找出这些被完全挡住的“倒霉蛋”把它们也从渲染队列里踢出去。那么既然两者都这么好为什么还要谈“混合策略”呢因为在实际项目中尤其是复杂场景里单一策略往往力不从心。视锥剔除对视野内的遮挡无能为力而传统的实时遮挡剔除如Unity的Occlusion Culling虽然强大但其CPU计算开销在动态物体多、场景变化快的情况下可能成为新的性能瓶颈。混合策略的核心思想就是根据场景的具体情况静态/动态物体比例、摄像机运动模式、硬件性能目标将视锥剔除的“快”和遮挡剔除的“准”结合起来取长补短实现渲染负载的最优分配。这篇文章我就结合自己踩过的坑和优化过的项目来详细拆解这套混合策略的实战玩法目标是让你不仅能理解原理更能直接应用到自己的项目里看到帧率的切实提升。2. 核心原理深度拆解视锥与遮挡如何工作在谈混合之前我们必须把两个基础技术的原理和局限吃透。很多优化方案出问题根源就在于对基础工具的理解有偏差。2.1 视锥剔除摄像机的第一道过滤网视锥剔除是渲染管线中最早发生、也是开销最低的剔除阶段。它的判断依据纯粹是几何关系一个物体更准确说是其包围盒是否与摄像机的视锥体相交。技术实现浅析 Unity中每个渲染物体都有一个Renderer组件其bounds属性定义了物体的轴对齐包围盒。在CPU端Unity会快速检测这个AABB与摄像机视锥体六个平面的位置关系。如果包围盒完全在任一平面的“外侧”则该物体被剔除。这个计算非常高效因为只涉及简单的平面方程点积运算。它的优势与局限优势计算开销极低是必须开启的“保底”优化。局限它只关心“是否在视野内”不关心“在视野内是否被挡住”。在复杂的城市峡谷或茂密森林中大量物体虽在视锥内但彼此遮挡严重视锥剔除对此完全无效。注意视锥剔除的效率与物体包围盒的紧密程度直接相关。一个模型如果用了非常“宽松”的包围盒可能会导致它在明明已经移出视野时因为包围盒的一个角还挂在视锥边上而继续被渲染。对于复杂模型或粒子系统手动调整或生成更紧密的包围盒有时是必要的优化步骤。2.2 遮挡剔除揪出视野内的“隐身者”遮挡剔除的目标就是解决视锥剔除的局限。它的核心问题是给定摄像机的当前位置和朝向哪些物体虽然在其视锥体内但被离摄像机更近的其他物体遮挡物完全挡住了Unity主要提供了两种实现路径1. 静态烘焙Occlusion Culling 这是最常用、对运行时性能影响最小的方式。它在编辑阶段进行。流程你需要将场景中不会移动的物体如建筑、地形标记为Occluder Static作为遮挡物和Occludee Static作为被遮挡物。然后通过Window Rendering Occlusion Culling打开面板设置分辨率等参数后点击Bake。原理烘焙器会将场景的包围盒空间体素化想象成切成无数个小立方体格子然后从多个预设的视角发射射线计算每个体素格子的可见性并将结果存储为一张“遮挡数据图”。运行时游戏运行时摄像机的位置会映射到这张数据图上快速查询哪些物体集合在当前视角下不可见。2. 动态实时剔除 对于动态物体或者无法预知移动路径的情况需要实时计算。Unity自身的高级渲染管线如URP/HDRP集成了一些实时遮挡剔除算法开发者也可以借助Occlusion Area组件或编写自定义脚本利用硬件遮挡查询如OcclusionQuery来实现。硬件遮挡查询GPU会先渲染一批简单代理几何体通常是物体的简化包围盒然后查询这些代理是否被最终渲染到了画面上。如果没有则说明该物体被完全遮挡。这个过程是异步的有1帧左右的延迟。它的优势与挑战优势能显著减少实际提交到GPU的绘制调用Draw Calls和三角面数提升渲染效率特别是在遮挡严重的场景中效果惊人。挑战静态烘焙只适用于静态环境。动态物体无法被烘焙进去作为有效的遮挡物但可以被静态物遮挡。烘焙数据会增加包体和内存占用。实时剔除CPU或GPU计算开销较高。硬件查询有延迟可能导致物体在从遮挡中突然出现时有一帧的“闪现”现象。如果管理不当查询本身的开销可能超过它节省的渲染开销。3. 混合策略设计与选型思路理解了各自的优缺点混合策略的设计思路就清晰了用最低的开销实现尽可能高的剔除率。没有一个放之四海而皆准的配方关键是根据你的场景配方下药。3.1 策略一静态为主动态为辅适用于大型开放世界/MMO场景这是最常见、最稳健的混合策略。场景中大部分建筑、地形、植被都是静态的只有玩家、NPC、车辆等是动态的。核心方案对静态环境进行精细的遮挡烘焙这是性能收益的基石。确保所有大型静态物体都正确标记。烘焙时需要平衡Smallest Occluder最小遮挡物和Smallest Hole最小孔洞参数。值设得太小烘焙时间巨长数据量也大值设得太大会漏掉很多有效的遮挡关系降低剔除效率。我的经验是对于大型户外场景可以从默认值开始然后重点观察那些由栅栏、镂空装饰等形成的“漏洞”是否导致了错误的剔除。对动态物体实施保守的视锥简单动态遮挡视锥剔除无条件开启这是动态物体的第一道防线。动态遮挡不要试图为每个动态物体都做复杂的实时遮挡查询。相反采用“分层”思想大型动态物体如BOSS、载具可以为其启用硬件遮挡查询因为它们自身可能遮挡大片区域收益较高。中小型动态物体如玩家、小怪通常只作为被遮挡物。它们能否被剔除主要依赖于静态烘焙数据。也就是说一个玩家跑进房子里他会被房子的墙壁从渲染中剔除因为房子是静态遮挡物但他自己无法剔除房子后面的其他静态物体。使用Occlusion Area在复杂的动态区域如一个有很多NPC的广场可以放置Occlusion Area组件。它会根据区域内的物体动态生成一个简化的遮挡代理辅助剔除判断比纯硬件查询开销低。实操心得 烘焙完成后一定要用Occlusion Culling面板的Visualization视图在编辑器里跑一遍。你会以摄像机的视角直观地看到哪些物体被剔除了显示为红色。经常能发现一些意外被剔除的物体比如因为烘焙参数问题或者该被剔除却没剔除的物体比如漏标了Static标签。这是调试剔除问题最快的方法。3.2 策略二实时优先静态简化适用于高度动态的竞技场/VR场景有些场景几乎全是动态物体比如一个可被大量破坏的竞技场或者一个物体可以被玩家随意抓取摆放的VR应用。静态烘焙在这里用处不大。核心方案强化实时视锥剔除确保所有动态物体的碰撞体或渲染器包围盒尽可能紧密。对于由大量小零件组成的复杂物体考虑使用一个简化的、合并的包围盒来进行视锥剔除计算而不是每个零件单独计算。采用高效的GPU驱动实时遮挡深度缓冲复用Depth Pre-Pass在正式渲染前先以极简的着色器快速渲染一遍场景中不透明物体的深度信息到一张纹理Z-Buffer。在后续主渲染中每个像素在着色前可以先比较自己的深度值与深度缓冲中的值。如果当前像素深度更大即更远且已被前面的物体完全覆盖则可以提前终止着色计算。这更多是GPU端的优化但需要渲染管线支持。基于软件光栅化的遮挡剔除一些高性能框架会使用CPU上的多线程软件光栅化用非常低分辨率的缓冲区来快速判断物体的可见性。这对大量动态小物体如子弹、碎片的剔除非常有效。在Unity中你可以通过Jobs System和Burst Compiler来自行实现一个简化版但门槛较高。将静态物体“动态化”处理对于少数大型静态背景可以将其视为一个特殊的“动态”物体为其手动创建始终存在的遮挡区域或者将其拆分成多个部分以适应动态的遮挡查询。注意事项 实时方案对CPU/GPU的计算能力提出挑战。必须进行严格的性能剖析Profiling。使用Unity Profiler重点观察Rendering.Culling和WaitForJob如果用了Job System的时间。如果剔除本身花费的时间超过了它节省的渲染时间那么这个方案就是失败的需要回退到更简单的策略。3.3 策略三分块加载与剔除结合适用于超大型无缝地图对于手机上的开放世界游戏内存和计算力都是硬约束。混合策略在这里要升级为“分治”策略。核心方案场景流式加载与卸载将世界划分为多个区块Chunk。只加载玩家所在区块及相邻1-2个区块。区块内的混合剔除对于已加载的区块内部采用“静态为主动态为辅”的策略进行精细的遮挡烘焙。对于区块的边界要特殊处理。因为相邻区块未加载原本的遮挡物可能消失导致本区块内物体被错误地渲染。通常的解决办法是在区块边界放置一些永不会卸载的“虚拟遮挡体”如远景雾、地形过渡带或者放宽边界区域的剔除条件。层级细节LOD与剔除联动不要将LOD和剔除视为独立系统。一个物体在根据距离切换LOD级别时其用于遮挡剔除的代理包围盒也应该相应切换。一个高模建筑可以作为有效的遮挡物但其切换成的低模甚至Billboard可能就不再具备遮挡能力需要在剔除系统中将其标记为“弱遮挡物”或仅作为被遮挡物。避坑技巧 在实现场景流式加载时最常见的剔除Bug就是“物体闪现”——物体在应该出现的时候没出现或者不该出现时出现了。这往往是因为剔除系统的更新频率和场景加载/卸载的时机不同步。确保在加载一个新场景区块后强制更新一次遮挡剔除系统例如在下一帧开始时手动调整摄像机位置触发重算可以避免大部分这类问题。4. 实战配置与性能剖析指南理论说再多不如动手配一遍。我们以一个典型的第三人称3D游戏场景为例配置一套混合策略并用Profiler验证效果。4.1 静态遮挡烘焙详细步骤假设我们有一个包含街道、楼房和少量绿化的小镇场景。物体标记选中所有街道、楼房、山体等永久固定的模型。在Inspector右上角点击Static下拉框确保勾选Occluder Static和Occludee Static。通常直接勾选最上方的Static复选框会同时启用多项但最好确认一下遮挡相关项已勾选。对于像铁丝网、栅栏这种有大量空隙的物体需要谨慎。如果希望它遮挡后面的物体就标记为Occluder Static如果希望它后面的物体能被看见就不要标记为遮挡物或者调整烘焙参数。烘焙参数设置打开Window Rendering Occlusion Culling切换到Bake面板。Smallest Occluder决定多小的物体可以被视为遮挡物。对于小镇墙壁、汽车大小比如1米的物体应该能遮挡。可以设为100厘米。值越小烘焙越精细时间越长。Smallest Hole决定多小的缝隙可以透过去看到后面。对于窗户你希望玩家能透过窗户看到屋内的部分陈设那么这个值应该小于窗户的尺寸。可以设为50厘米。Backface Threshold这是一个高级参数默认为100。降低这个值如5可以让系统将那些背对摄像机的薄面片如一片树叶的背面也视为潜在的遮挡物提高剔除率但可能增加错误剔除的风险。对于户外植被多的场景可以尝试调低。执行烘焙并查看点击Bake按钮等待完成。时间取决于场景复杂度和参数。烘焙后切换到Visualization面板并点击Play进入运行模式。在Scene视图中你会看到被剔除的物体显示为红色。移动摄像机观察剔除是否符合预期。4.2 动态物体处理脚本示例对于场景中巡逻的NPC动态物体我们写一个简单的脚本让它除了享受静态遮挡外还能在远离摄像机时主动禁用渲染器这是一种粗粒度的“距离剔除”作为补充。using UnityEngine; public class DynamicObjectCuller : MonoBehaviour { private Renderer objectRenderer; private Transform mainCameraTransform; public float cullDistance 30.0f; // 超过30米禁用渲染 public float checkInterval 0.5f; // 每0.5秒检查一次避免每帧检查 private float timer; void Start() { objectRenderer GetComponentRenderer(); if (Camera.main ! null) { mainCameraTransform Camera.main.transform; } timer checkInterval; } void Update() { timer - Time.deltaTime; if (timer 0f) { timer checkInterval; if (objectRenderer null || mainCameraTransform null) return; float distanceToCamera Vector3.Distance(transform.position, mainCameraTransform.position); // 仅当物体在摄像机远裁剪面之内时才根据我们的自定义距离进行显隐控制 // 更完整的做法应该先进行视锥判断这里为简化示例省略 if (distanceToCamera cullDistance) { if (objectRenderer.enabled) { objectRenderer.enabled false; } } else { if (!objectRenderer.enabled) { objectRenderer.enabled true; } } } } }这个脚本非常基础但它说明了一个思想混合策略不局限于Unity内置功能你可以根据游戏逻辑添加任何自定义的剔除规则。比如对于潜行游戏可以写一个规则当玩家在敌人视线外时禁用远处无关敌人的渲染。4.3 使用Profiler进行量化分析配置好了效果如何必须用数据说话。打开ProfilerWindow Analysis Profiler。进入游戏运行模式。观察关键数据CPU Usage查看Rendering模块下的Culling耗时。这是剔除系统总耗时。优化后这个时间不应显著增加对于静态烘焙它几乎不变对于实时方案需密切关注。GPU Usage观察Batches和SetPass Calls。成功的剔除会显著降低这两个数值。例如在室内看向墙壁时墙壁后的物体被剔除Batches应该大幅下降。内存在Memory区域查看Occlusion Culling Data的大小了解烘焙数据的内存占用。对比测试创建一个测试场景包含一个密集的物体阵列。第一轮关闭遮挡剔除只开视锥剔除。记录平均帧率、Batches和Culling耗时。第二轮开启配置好的混合剔除。再次记录数据。对比两者。理想的状况是Batches大幅下降帧率提升而Culling耗时仅有小幅增加或基本不变。5. 常见问题排查与进阶技巧即使按照最佳实践配置在实际项目中还是会遇到各种稀奇古怪的问题。这里我整理了一份“踩坑实录”。5.1 问题速查表问题现象可能原因排查与解决方案物体闪烁时隐时现1. 实时硬件遮挡查询延迟导致。2. 静态烘焙数据中物体因Smallest Hole参数被误判在孔洞边缘可见性在帧间波动。3. LOD切换与剔除不同步。1. 对于动态物体考虑禁用硬件查询或接受1帧延迟。2. 在Occlusion Visualization中观察该物体调整Smallest Hole参数或将该物体从Occludee Static中排除。3. 确保LOD组件的包围盒Renderer.bounds能正确代表当前LOD级别的几何体。该被遮挡的物体依然被渲染1. 物体未标记为Occludee Static。2. 遮挡物未标记为Occluder Static。3. 遮挡物太薄如一面墙只有单面Backface Threshold设置过高。4. 动态物体无法被静态烘焙数据剔除。1. 检查物体Static标记。2. 检查遮挡物Static标记。3. 尝试调低Backface Threshold或为薄墙添加一个具有厚度的碰撞体作为遮挡代理。4. 这是预期行为。考虑使用Occlusion Area或将动态物体“静态化”处理如果其移动范围有限。烘焙时间过长或数据巨大1.Smallest Occluder或Smallest Hole参数设置过小。2. 烘焙范围Occlusion Culling面板中的View Cell Size过大或过密。3. 场景中标记为Static的细小物体过多如大量碎石。1. 适当调大这两个参数找到性能与质量的平衡点。2. 非开放世界场景可以适当增大View Cell Size。3. 对于大量细小静态物体考虑将它们合并成一个大的Mesh或者取消其Occluder Static标记只保留Occludee Static。在移动平台上帧率反而下降1. 复杂的实时剔除脚本如每帧计算大量距离消耗了过多CPU。2. 静态烘焙数据过大导致内存访问和查询开销增加。1. 使用Profiler定位CPU热点简化或降低自定义剔除逻辑的执行频率如上面脚本的checkInterval。2. 简化烘焙参数减少烘焙数据量。对于移动端可以接受稍低的剔除率以换取更小的内存和CPU开销。编辑器里剔除正常打包后失效1. 打包时未包含烘焙数据。2. 场景加载顺序问题导致剔除系统在场景加载完毕前初始化。1. 确保在Build Settings中场景的Occlusion Data已被包含。检查Player Settings Other Settings中的相关选项。2. 在场景加载完成的回调如SceneManager.sceneLoaded中尝试手动触发一次摄像机剔除更新。5.2 进阶技巧自定义遮挡查询与Job System对于追求极致性能的项目可以尝试用Unity的Job System和Burst Compiler编写一个并发的、基于软件光栅化的简易遮挡系统用于处理大量同质动态物体比如一片草地、一群飞鸟。核心思路将摄像机视锥体投影到近裁剪面上一个低分辨率如32x18的“潜在可见网格”。将所有动态物体的简化包围盒AABB投影到同一个2D网格上。使用多线程Job并行计算每个网格单元Cell的“深度”。如果一个物体的投影覆盖的网格单元其深度值都比当前记录的值更远即被挡住则该物体在本帧很可能被遮挡。将判定结果写回主线程用于设置Renderer.enabled。优势高度可控分辨率、判定逻辑完全自己掌握。可并行利用多核CPU效率高。无延迟计算结果立即生效无硬件查询的帧延迟。挑战实现复杂需要较强的数学和编程能力。并非银弹它只是一种近似在物体边缘、透明物体处理上可能不精确。维护成本需要随项目迭代和Unity版本更新进行维护。我个人的建议是除非你的项目有非常明确的、大量动态小物体的性能瓶颈并且经过Profiler证实现有方案无法解决否则不要轻易尝试自研复杂的剔除系统。Unity内置的混合方案经过合理配置已经能解决95%以上的性能问题。把时间花在美术资源的优化、Draw Call的合并和LOD的调整上性价比往往更高。最后再分享一个小技巧在项目后期进行性能优化时建立一个“压力测试场景”。这个场景集中了你项目中所有最复杂的模型、最多的动态物体和最差的摄像机角度。任何剔除策略的修改都在这个场景里进行测试和对比。这样可以快速验证策略的有效性避免在复杂的主场景中盲目调整事半功倍。