☰
Unity 遮挡剔除全流程解析:看不见的,就不画
2026/10/17 1:42:55 网站建设 项目流程

一句话:遮挡剔除就是在渲染前判断"哪些物体被墙挡住了",被挡住的直接跳过不画,从而减少 Draw Call,降低 CPU 和 GPU 的压力。


一、生活里的例子:站在客厅里

你站在自家客厅:

  • 身后的东西看不到 → 不用画(这是视锥剔除)
  • 卧室里的床在你前方,但被墙挡住了 → 也不用画(这是遮挡剔除)
  • 卧室门开着,从门缝能看到一角 → 这时才需要画

如果不做遮挡剔除,GPU 会把整栋楼的家具都画一遍,再被墙盖住。画了,但白画了。


二、Unity 里的三道"过滤网"

每一帧,物体要经过几层筛选才会真正被绘制:

场景里所有物体(比如 5000 个) │ ▼ ① 视锥剔除(自动,总是开启) 不在摄像机视野范围内的 → 去掉 剩 2000 个 │ ▼ ② 遮挡剔除(需要手动设置和烘焙) 在视野内,但被墙挡住的 → 去掉 剩 300 个 │ ▼ ③ 提交渲染(合批、LOD、Draw Call) 真正画出来的 300 个
剔除方式判断依据是否默认开启
视锥剔除在不在摄像机的"视野锥体"里✅ 自动
遮挡剔除有没有被别的东西挡住❌ 需要烘焙
距离剔除离得太远就不画(camera.layerCullDistances)❌ 需要代码设置

三、Unity 内置遮挡剔除的核心思路:提前算好

Unity 内置方案使用第三方库Umbra,思路是:

在编辑器里提前把"从哪里能看到哪里"算好存起来(烘焙),游戏运行时直接查表。

为什么要提前算?因为每帧实时精确计算"谁挡住了谁"太贵;提前算好后,运行时只需要很少的 CPU 开销。

代价:只有不会移动的物体才能当"挡板"。


四、全流程:七个步骤

1. 标记物体 谁是挡板?谁能被挡? 2. 划定区域 摄像机会去哪些地方? 3. 设置门 哪些通道会开关? 4. 调整参数 格子多细? 5. 烘焙 生成可见性数据 6. 可视化检查 剔除得对不对? 7. 性能验证 真的变快了吗?

第 1 步:标记物体(Static 标记)

选中物体,在 Inspector 右上角Static下拉菜单里勾选:

标记意思适合什么
Occluder Static我是挡板,能挡住别人大墙、地板、山体、大建筑
Occludee Static我是被挡的,被挡住时可以不画桌椅、箱子、装饰物

一个物体可以两个都勾(比如一栋房子:既挡住后面的东西,自己也会被前面的山挡住)。

怎么选:

物体OccluderOccludee原因
大面墙✅✅又大又不透明
小茶杯❌✅太小,挡不住什么
玻璃窗❌✅透明的,挡不住视线
铁丝网、树叶❌✅有很多镂空
会移动的箱子❌❌(用下面的动态选项)不是静态物体

动态物体(会移动的)怎么办?
在它的 Mesh Renderer 上勾选Dynamic Occlusion(默认已勾选)。这样它可以被静态墙挡住而剔除,但不能挡住别人。

所以敌人、玩家、子弹走到墙后面时,依然会被剔除。


第 2 步:划定摄像机活动区域(Occlusion Area)

GameObject → 创建空物体 → Add Component → Occlusion Area

情况结果
不放 Occlusion AreaUnity 对整个场景都计算,数据大、烘焙慢
放了,并勾选Is View Volume只在摄像机会去的地方计算精细数据

射击游戏例子:玩家只在走廊和房间里走动,就只在这些地方放 Occlusion Area,天空、山后面这些摄像机去不了的地方不用算。


第 3 步:设置可开关的门(Occlusion Portal)

门关上时,门后的东西应该被剔除;门打开时,又要能看到。

做法:在门的位置放一个物体,加Occlusion Portal组件,调整大小正好盖住门洞。

usingUnityEngine;[RequireComponent(typeof(OcclusionPortal))]publicclassDoor:MonoBehaviour{OcclusionPortalportal;voidAwake(){portal=GetComponent<OcclusionPortal>();portal.open=false;// 一开始门是关的,门后的东西被剔除}publicvoidOpen(){portal.open=true;// 开门:门后可以被看到了// 播放开门动画……}publicvoidClose(){portal.open=false;// 关门:门后又可以被剔除}}

注意:

  • 门本身不要标记 Occluder Static。会动的门如果当挡板烘焙进数据,门打开以后,门后的东西还是被当成"被挡住",就看不见了。
  • Portal 的大小要略大于门洞,不然边缘可能漏剔或误剔。
  • 门开着时,Portal 不挡任何东西;关上时,它就像一面临时的墙。

第 4 步:调整烘焙参数

打开Window → Rendering → Occlusion Culling,切到Bake页签:

参数意思调大的效果调小的效果
Smallest Occluder多大的物体才算挡板(米)烘焙快、数据小,但小墙挡不住东西剔除更多,但数据大、烘焙慢
Smallest Hole多小的缝隙算"能看穿"小缝被当成堵死,可能误剔(物体突然消失)更准确,但数据更大
Backface Threshold背面检测阈值(%)100 = 不做额外优化调低可以去掉"在墙里面才能看到"的无效数据,减小体积

射击游戏推荐的起点:

场景类型Smallest OccluderSmallest Hole
室内走廊、房间1~20.1~0.25
城市街道3~50.25
大型户外5~100.5

调参的原则:先用默认值烘焙一次,看到问题再调。物体"闪烁消失"就调小 Smallest Hole;数据太大就调大 Smallest Occluder。


第 5 步:烘焙

点击窗口右下角的Bake。

Unity 会做这些事:

1. 收集所有 Occluder Static 和 Occludee Static 物体 2. 把场景空间切成很多小格子(Cell) 3. 计算:站在每个格子里,能看到哪些格子 4. 识别门(Portal)的位置,记录"门开 / 门关"时的连通关系 5. 把结果压缩,存成一份数据文件

烘焙结果保存在和场景同名的文件夹里(OcclusionCullingData.asset),随场景一起打包。

多场景叠加加载(Additive)的注意事项:
如果游戏是"主场景 + 多个子场景叠加",要把这些场景同时打开再烘焙,Unity 会生成一份共享数据。分开烘焙的话,场景之间的遮挡关系不会被计算。


第 6 步:可视化检查

还是在 Occlusion Culling 窗口,切到Visualization页签,然后在 Scene 视图里选中摄像机:

显示选项用途
Camera Volumes看摄像机能去的区域(格子划分)
Visibility Lines看摄像机的视线投射到哪里
Portals看门的位置和连通关系

拖动摄像机走一圈,被剔除的物体会直接从 Scene 视图里消失。

重点检查:

  • 墙后面的东西有没有消失?(没消失 → 墙没标 Occluder,或者太小)
  • 视野里的东西有没有不该消失却消失了?(→ Smallest Hole 太大,或者透明物体被当成了挡板)
  • 走到门口、拐角时,有没有物体**“突然出现”**?

还有一个常被忘记的开关:摄像机组件上的Occlusion Culling复选框必须勾上(默认是勾上的),否则烘焙了也不生效。


第 7 步:性能验证

做了遮挡剔除,不代表一定变快。要用数据说话:

工具看什么
Game 视图 StatsBatches(批次数)、Tris(三角形数)有没有明显下降
Frame Debugger一帧里到底画了哪些物体
Profiler → CPU搜索Culling相关的耗时,看剔除本身花了多少时间
Profiler → RenderingDraw Call 和 SetPass Call 数量

判断标准:

省下的渲染时间 > 剔除本身的耗时 → 值得 省下的渲染时间 ≤ 剔除本身的耗时 → 不值得,可能反而变慢

典型情况:开阔的户外场景,大部分东西本来就互相看得见,剔除几乎剔不掉什么,反而白白多了查询开销。


五、运行时每一帧发生了什么

每一帧,摄像机开始渲染前: 1. 拿到摄像机的位置和朝向 2. 查烘焙数据:摄像机现在在哪个格子里? 3. 从这个格子出发,沿着"能看到的格子"往外找 ├─ 遇到开着的门(Portal.open = true)→ 继续往门后找 └─ 遇到关着的门 → 停下,门后不再找 4. 同时结合视锥:不在视野范围内的格子直接跳过 5. 得到"可见格子"列表 ├─ 静态物体:在可见格子里的 → 保留,否则剔除 └─ 动态物体:用包围盒检查是否被静态挡板挡住 → 挡住就剔除 6. 剩下的物体交给渲染管线:合批、LOD、提交 Draw Call

关键点:运行时没有实时计算"谁挡住了谁",只是查表 + 沿格子走一走,所以 CPU 开销很小。这就是"烘焙"的价值:把贵的计算挪到了开发阶段。


六、射击游戏的实际案例

假设做一个室内 CQB(近距离战斗)射击游戏:一栋三层建筑,每层 10 个房间。

设置做法
墙、地板、天花板Occluder + Occludee Static
桌椅、柜子、装饰Occludee Static
窗户玻璃只勾 Occludee
可破坏的墙不要当 Occluder(破坏后会看错),可以放一个 Portal,墙被打破时open = true
房间门Occlusion Portal + 上面的 Door 脚本
敌人、玩家、子弹、弹壳动态物体,保持 Dynamic Occlusion 勾选
Occlusion Area只覆盖建筑内部和玩家能走到的室外区域

效果(示意数量级):

不开遮挡剔除:玩家站在一楼房间里,Batches ≈ 1200(全楼的家具都在画) 开启遮挡剔除:同一个位置,Batches ≈ 150(只画当前房间和门口能看到的部分)

七、Unity 内置方案的局限与其他方案

内置的 Umbra 方案很好用,但不是万能的:

局限说明
只有静态物体能当挡板会移动的大卡车挡不住后面的东西
需要烘焙程序化生成的关卡(随机地图)无法提前烘焙
场景改动就要重新烘焙摆了一面新墙,忘记重新烘焙,剔除就不准
大型开放世界效果有限数据量巨大,且遮挡关系少

其他可选方案:

方案思路适合
GPU 遮挡剔除(Unity 6 起,URP/HDRP)配合 GPU Resident Drawer,用上一帧的深度图在 GPU 上判断遮挡,不需要烘焙,动态物体也能当挡板物体数量多、支持 Compute Shader 的平台
手动分区开关按房间/区域分组,玩家进入某区域时用代码SetActive打开或关闭其他区域关卡结构简单、房间分明的游戏
距离剔除camera.layerCullDistances给不同 Layer 设不同的最远绘制距离小装饰物、草、碎石
LOD + 远处简化远处不剔除,但用低面数模型开放世界
自定义 CullingGroupCullingGroupAPI 查询包围球的可见性和距离档位,用于决定 AI、特效、动画是否更新控制逻辑开销(看不见的敌人不跑动画)

实际项目常常组合使用:内置遮挡剔除处理室内,距离剔除处理小物件,LOD 处理远景,CullingGroup 处理看不见的特效和动画。

CullingGroup 小例子:看不见的敌人不播动画,省 CPU。

usingUnityEngine;publicclassEnemyVisibility:MonoBehaviour{publicAnimator[]enemies;CullingGroupgroup;BoundingSphere[]spheres;voidStart(){group=newCullingGroup();group.targetCamera=Camera.main;spheres=newBoundingSphere[enemies.Length];for(inti=0;i<enemies.Length;i++)spheres[i]=newBoundingSphere(enemies[i].transform.position,1.5f);group.SetBoundingSpheres(spheres);group.SetBoundingSphereCount(enemies.Length);group.onStateChanged=OnStateChanged;}voidUpdate(){// 敌人会移动,每帧更新包围球位置for(inti=0;i<enemies.Length;i++)spheres[i].position=enemies[i].transform.position;}voidOnStateChanged(CullingGroupEvente){// 变得可见 → 打开动画;变得不可见 → 关闭动画if(e.hasBecomeVisible)enemies[e.index].enabled=true;if(e.hasBecomeInvisible)enemies[e.index].enabled=false;}voidOnDestroy(){group.Dispose();// 必须释放,否则内存泄漏group=null;}}

这里的"可见"同样会考虑烘焙好的遮挡数据:敌人在墙后面时,hasBecomeInvisible也会触发。


八、常见的坑

坑现象解决
摄像机没勾 Occlusion Culling烘焙了但完全没效果检查 Camera 组件的复选框
透明物体当挡板玻璃后面的东西消失了玻璃、栅栏、树叶不要勾 Occluder
会动的门当挡板开门后门后一片空门用 Portal,不要标 Occluder
改了场景没重新烘焙新墙不挡东西,或删掉的墙还在"挡"关卡定稿后再烘焙,改动后记得重烘
Smallest Hole 太大物体在拐角处"闪烁"或突然出现调小 Smallest Hole
没放 Occlusion Area烘焙极慢,数据文件很大只覆盖摄像机能去的区域
多场景分开烘焙场景之间互相不剔除同时打开所有场景一起烘焙
摄像机跑到墙里面大片物体错误消失防止摄像机穿墙(摄像机碰撞)
开阔场景硬上遮挡剔除帧率没提升,甚至下降先用 Profiler 确认瓶颈在渲染
以为剔除也会停掉脚本看不见的敌人还在跑 AI 和动画遮挡剔除只管渲染,逻辑要用 CullingGroup 自己控制

九、什么时候值得用?

场景是否推荐
室内、走廊、地下城、楼房✅ 非常适合,效果明显
城市街道(高楼挡视线)✅ 适合
俯视角 2D/2.5D 射击游戏❌ 几乎没东西会被挡,视锥剔除就够了
大型开阔户外⚠️ 效果有限,优先用 LOD + 距离剔除
随机生成的关卡❌ 无法烘焙,考虑 GPU 遮挡剔除或手动分区
移动端、低端设备的 3D 室内游戏✅ 特别值得,Draw Call 是移动端常见瓶颈

一句话总结

Unity 遮挡剔除 = 标记挡板和被挡物 → 划定摄像机区域 → 用 Portal 处理门 → 调参烘焙 → 可视化检查 → 用 Profiler 验证。
它的本质是把"谁挡住谁"的计算提前到编辑器里做完,运行时只查表,所以开销很小;但只有静态物体能当挡板,最适合室内、结构封闭的场景。是否真的变快,以 Profiler 的数据为准。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询