三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Unity遮挡剔除深度解析:Occluder与Occludee实战勾选策略

Unity遮挡剔除深度解析:Occluder与Occludee实战勾选策略

1. 项目概述:为什么你的Static勾选可能是错的?

在Unity项目里,尤其是那些场景稍微复杂点的3D项目,性能优化是个绕不开的话题。很多开发者,特别是刚接触Unity不久的朋友,一听到“优化”两个字,下意识就会打开场景里所有静态物体的Inspector面板,把那个“Static”复选框挨个勾上。心里想着:“勾上Static总没错吧?能让引擎优化,何乐而不为?” 我以前也这么干过,直到项目在移动设备上跑得跟幻灯片一样,或者烘焙Occlusion Culling(遮挡剔除)时发现要么没效果,要么烘焙时间长得离谱,甚至烘焙数据异常庞大,我才意识到问题没那么简单。

这个“Static”复选框,在Unity的语境下,它不是一个单一的开关,而是一个包含了多种优化系统(如Batching、Navigation、Occlusion Culling、Reflection Probe等)的“总开关”。我们今天要深挖的,就是它对于Occlusion Culling这个核心渲染优化技术的影响。具体来说,是Occluder StaticOccludee Static这两个子选项到底该怎么用。网络上的讨论很多,官方文档的解释有时又显得过于简略,导致很多开发者,包括一些有经验的,对这两个概念的理解都存在混淆。最常见的误区就是把所有静态物体都同时勾上OccluderOccludee,或者完全搞反了它们的角色,最终导致遮挡剔除系统要么不工作,要么产生错误的剔除结果,画面出现“穿帮”(物体在应该被看到时消失了)。

所以,这篇文章的目的就是彻底讲清楚Occluder(遮挡物)和Occludee(被遮挡物)在Unity遮挡剔除系统中的定义、区别、以及最关键的——实战中的勾选策略。我会结合具体的场景案例,告诉你什么样的物体应该只当Occluder,什么样的应该只当Occludee,什么样的可以身兼二职,以及什么样的应该一个都不勾。理解这些,不仅能让你正确使用遮挡剔除,更能让你对Unity的静态批处理、动态批处理等其他优化手段有更深刻的认识,避免因错误的Static设置引发一系列连锁的性能问题。

2. 核心概念拆解:Occluder与Occludee到底是谁?

在深入实操之前,我们必须像认识新朋友一样,把OccluderOccludee这两个核心概念掰开揉碎了理解。很多误解都源于对它们基础定义的不清晰。

2.1 Occluder:场景中的“墙”与“盾牌”

你可以把Occluder(遮挡物)想象成场景中一堵坚实的墙、一座大山,或者一个巨大的集装箱。它的核心职责是:挡住摄像机看向其后方物体的视线。在遮挡剔除的计算中,Occluder是主动的“施法者”。

关键特性:

  1. 实体性与封闭性:一个理想的Occluder应该是一个视觉上“密不透风”的实体。从摄像机视角看过去,它不应该有大的孔洞或缝隙。一堵完整的墙是完美的Occluder,而一个铁丝网围栏则不是。
  2. 相对尺寸Occluder需要在屏幕空间占有一定的面积,才能有效地遮挡其后面的物体。一个非常细小的物体,比如一根铅笔,即使它处在其他物体前面,也几乎无法在像素级别形成有效的遮挡,将其标记为Occluder只会增加不必要的计算开销,没有实际收益。
  3. 不透明性:这是最容易被忽略的一点。透明或半透明的物体(如玻璃窗、粒子效果)绝不能标记为Occluder。因为从逻辑上讲,你能透过它看到后面的东西,它就不具备“遮挡”的能力。如果强行标记,Unity会认为它挡住了后面,从而错误地剔除掉本应可见的物体,导致画面错误。

注意Occluder Static的勾选,意味着你告诉Unity:“这个物体在运行时不会移动、旋转或缩放,并且它的形状足够‘结实’,可以用来判断它后面有没有东西能被看到。” Unity在烘焙(Bake)遮挡数据时,会将这些物体的几何信息用于计算可见性。

2.2 Occludee:场景中的“演员”与“道具”

Occludee(被遮挡物)则是场景中那些可能被“墙”挡住的物体。比如墙后面的一个箱子、一座房子后面的树、或者巨大雕像脚下的一朵花。它的角色是被动的“承受者”。

关键特性:

  1. 可被遮挡性:标记为Occludee,等于向Unity声明:“我这个物体是可以被其他Occluder挡住的,如果摄像机看不到我,请放心地别渲染我。” 这是遮挡剔除能提升性能的根本——通过不渲染看不见的东西来节省GPU的绘制调用(Draw Calls)和像素着色开销。
  2. 普遍性:场景中绝大多数不透明的静态物体,理论上都可以是Occludee。因为只要有一个足够大的Occluder在它前面,它就有可能被挡住。
  3. 与尺寸无关:一个物体无论多小,只要它是不透明的静态物体,都可以是Occludee。因为即使是一个小石子,也可能被一堵墙完全挡住。

注意Occludee Static的勾选,意味着你告诉Unity:“这个物体是静态的,并且你可以在判断它被其他Occluder完全挡住时,安全地跳过对它的渲染。” 如果一个物体没有被勾选Occludee,那么无论它前面有没有Occluder,Unity在运行时都会认为它“永远可见”,从而始终尝试渲染它,这会使遮挡剔除对该物体完全失效。

2.3 关系的核心:非互斥且常共存

这是最容易产生困惑的地方。从字面意思看,“遮挡物”和“被遮挡物”像是一对反义词,但在Unity的遮挡剔除系统中,它们不是互斥的,一个物体完全可以同时是OccluderOccludee

为什么可以共存?想象一下游戏场景中的一个石头房子。对于房子内部的家具而言,房子的墙壁和屋顶是Occluder(遮挡了家具)。但同时,这座房子本身,也可能被更远处的一座大山(另一个Occluder)所遮挡。此时,这座石头房子就同时扮演了两个角色:

  • 对于它内部的家具,它是Occluder
  • 对于远处的大山,它是Occludee

因此,一个物体的“身份”是相对的,取决于观察的层级关系。在复杂的场景中,一个物体兼具两种身份是非常普遍的情况。官方文档推荐为许多物体同时勾选两者,正是基于这种普遍的相对性。

一个绝对不能共存的例外:就是前面提到的透明物体。一个透明的物体(如玻璃)既不能有效地遮挡别人(所以不应是Occluder),同时它本身虽然可能被遮挡,但将其标记为Occludee也可能有问题,因为透明渲染通常依赖深度排序,错误的剔除可能导致渲染顺序混乱。对于完全透明的物体,最安全的做法是两个都不勾,将其排除在静态遮挡剔除系统之外,或者使用其他管理方式(如按距离裁剪)。

3. 实战勾选策略:从理论到场景配置

理解了概念,我们进入最关键的实战环节。面对场景中成百上千的物体,究竟该如何决定每个物体Occluder StaticOccludee Static的勾选状态?我总结了一个“四象限”分类法,你可以对照着对你的场景资产进行快速归类。

3.1 分类一:大型、坚实、不透明的静态物体(两者都勾)

这是最标准、最常见的一类。它们构成了场景的主体结构和视觉屏障。

典型物体:

  • 建筑的外墙、屋顶、地板。
  • 巨大的岩石、山体。
  • 厚重的集装箱、城堡的城墙。
  • 大型的不透明雕塑、纪念碑。

勾选策略:Occluder Static✅ &Occludee Static

原因分析:

  1. 它们体积大且封闭,是完美的Occluder,能有效遮挡其后方的大量物体(如建筑内部的摆设、山后的树林)。
  2. 它们自身也可能被更大的物体遮挡(例如,一个小房子可能被一座大山挡住),因此也需要作为Occludee参与计算。

实操心得:对于这类物体,在导入Unity或制作预制体时,就可以在模型文件的导入设置或预制体根节点上预先设置好Static标签(包含Occlusion),形成规范。这样可以避免在场景搭建后期一个个去勾选的繁琐。

3.2 分类二:小型、琐碎或不封闭的静态物体(仅勾选Occludee)

这类物体通常作为场景的细节点缀,它们本身不太可能去有效遮挡其他物体,但自己却很容易被挡住。

典型物体:

  • 散落在地面上的小石块、瓶子、书本。
  • 栅栏、楼梯扶手(有缝隙)。
  • 链条、绳索(非常细小)。
  • 树叶茂密但透光的树冠(视觉上不封闭)。

勾选策略:Occluder Static❌ &Occludee Static

原因分析:

  1. 为何不是Occluder?它们要么太小,在屏幕上占据像素太少,遮挡计算性价比极低;要么本身有孔洞缝隙,无法形成有效遮挡。将其标记为Occluder只会徒增烘焙时的计算量和运行时数据量,而几乎不会带来任何有效的剔除优化。
  2. 为何是Occludee?它们完全可能被前面的墙壁、大型家具等物体挡住。当被挡住时,我们当然希望系统能剔除它们以节省性能。

注意:这里有一个性能权衡。如果场景中有成千上万个这样的小物体,即使只作为Occludee,也会增加烘焙数据复杂度。此时可以考虑是否真的需要它们参与遮挡剔除。对于极远处或重要性极低的小物件,或许可以通过LOD(多层次细节)或简单的视距裁剪来管理,而不是依赖遮挡剔除。

3.3 分类三:透明、半透明或视效物体(两者都不勾)

这类物体需要被特殊对待,因为它们的视觉特性与遮挡剔除的基本假设(不透明、实体)相冲突。

典型物体:

  • 窗户玻璃、水幕、全息投影。
  • 粒子系统(烟雾、火焰、魔法特效)。
  • 使用了透明或镂空Shader的物体(如铁丝网、树叶Billboard)。
  • 仅用于触发事件的碰撞体(Mesh Renderer被禁用,但Collider启用)。

勾选策略:Occluder Static❌ &Occludee Static

原因分析:

  1. 绝对不能是Occluder:这是铁律。透明物体无法遮挡其后的物体。如果错误标记,会导致其后方本应可见的物体被剔除,画面出现“空洞”。
  2. 谨慎作为Occludee:透明物体的渲染依赖于正确的深度排序和混合。如果被遮挡剔除系统过早地决定其不可见,可能会破坏渲染顺序,导致视觉错误。对于完全动态的粒子特效,其可见性更适合由粒子系统自身的生命周期和边界框(Bounds)来控制,而非静态的遮挡数据。

避坑技巧:对于像“带窗格的窗户”这种物体,需要拆解看待。窗框(不透明)部分可以按分类一处理,而玻璃(透明)部分则应该单独分离成一个子物体,并归为此类(两者都不勾)。确保在项目资源管理规范中明确这类特殊资产的Static设置要求。

3.4 分类四:特大背景或天空盒物体(仅勾选Occluder?或都不勾?)

这类物体比较特殊,通常是构成场景视觉边界的元素。

典型物体:

  • 环绕场景的远景山脉、天空盒几何体。
  • 永远不会被玩家“绕到后面去”的巨型背景板。

勾选策略:视情况而定,通常Occluder Static✅ &Occludee Static❌, 或者两者都不勾。

原因分析与选择:

  • 策略A(作为Occluder):如果这个背景物体(如环绕的群山)确实会遮挡其后面的其他景物(比如山后面的云层贴图或更远的山脉),那么它可以作为Occluder。但由于玩家永远不可能到这些山体的“后面”去,它自己不存在“被遮挡”的可能,因此Occludee无需勾选。
  • 策略B(不作为Occluder):如果这个背景物体只是一个纯粹的视觉背景(例如,一个半球形的天空穹顶),它不会遮挡任何实际游戏内容(因为所有内容都在它“内部”),那么它作为Occluder没有意义。同时,它也不可能被遮挡,所以Occludee也无意义。此时两者都不勾,将其排除在遮挡计算之外是最干净的。
  • 如何选择?取决于你的场景构成。一个简单的判断方法是:在场景中移动摄像机,看这个背景物体是否曾处于其他游戏对象和摄像机之间,并切实挡住了那些对象。如果是,用策略A;如果不是,用策略B。

为了更直观,我将这四类策略总结成下表,方便你快速查阅:

物体类型典型示例Occluder StaticOccludee Static核心原因与注意事项
大型坚实不透明体建筑外墙、山体、厚墙是有效的遮挡物,自身也可能被遮挡。主力优化对象。
小型琐碎静态体小石块、瓶子、栅栏自身遮挡效果差,标记为Occluder浪费资源;但需要被剔除。
透明/半透明/特效体玻璃、粒子、铁丝网透明物不能遮挡他物;其渲染与剔除逻辑特殊,需单独处理。
特大背景/天空物体远景山、天空穹顶✅ 或 ❌若遮挡他物则勾Occluder;若为纯背景则都不勾,避免无意义计算。

4. 工作流详解:从场景准备到数据烘焙

正确的勾选策略是基础,但要让遮挡剔除真正高效工作,还需要遵循一个完整的、可操作的工作流。下面我以创建一个室内小场景为例,拆解每一步的操作和意图。

4.1 第一步:场景分析与初步标记

在动手勾选任何复选框之前,先花点时间在Scene视图中浏览你的场景。使用飞行模式(按住鼠标右键+WSAD)快速穿梭,从玩家可能的视角观察。

  1. 识别主要遮挡体:找出那些大型的、连续的墙体、地面和大型家具。在心里将它们归类为“分类一”。
  2. 找出细节物体:识别出所有小摆设、装饰品。将它们归类为“分类二”。
  3. 揪出特殊物体:找到所有透明材质、粒子发射器。将它们归类为“分类三”,并确保它们的材质渲染模式(Rendering Mode)是TransparentFade
  4. 规划背景:明确哪些是永远不会被穿过的背景元素,决定处理策略(分类四)。

4.2 第二步:批量设置与层级管理

Unity提供了强大的批量操作和层级(Layer)管理功能,善用它们能极大提升效率。

  1. 使用图层辅助选择:在动手前,可以规划几个专用图层,如“Occluder_Static”、“Occludee_Only”、“Transparent_Ignore”。将物体按分类拖入相应图层。
  2. 批量勾选Static:在Hierarchy面板中,可以通过图层过滤选择同一类物体。选中所有“分类一”的物体,在Inspector右上角点击“Static”下拉菜单,勾选“Occlusion Static”(这会同时勾选Occluder和Occludee)。对于“分类二”的物体,批量勾选后,需要手动取消其中的Occluder Static,只保留Occludee Static。这可以通过编写一个小编辑器脚本批量完成,或者使用一些第三方工具。
  3. 检查预制体根节点:如果大量物体来自预制体,务必检查预制体根节点的Static设置。在预制体模式下设置好,所有实例都会继承,这是最规范的做法。

4.3 第三步:烘焙参数配置与执行

打开Window > Rendering > Occlusion Culling面板,这里才是决定烘焙质量和效率的核心。

  1. Occluder选项卡
    • Smallest Occluder:这是最重要的参数之一。它定义了能被当作Occluder的物体在屏幕上的最小像素尺寸。设置过高(如100),很多本应参与遮挡的中小型物体将被忽略,导致剔除不精确。设置过低(如2),会把大量细小物体也纳入计算,急剧增加烘焙时间和数据量,得不偿失。我的经验值:对于大多数第三人称或FPS游戏,从10-20开始测试是一个不错的起点。对于俯视角或战略游戏,可以设得更低(如5),因为单个物体在屏幕上可能更小。
    • Smallest Hole:定义Occluder上可以被忽略的“孔洞”的最大尺寸。如果一个物体上有比这个值小的缺口(比如门上的钥匙孔),系统会将其忽略,仍视其为完整遮挡物。通常保持默认(25)即可,除非你的场景有很多栅栏、网格这类物体。
  2. Bake选项卡
    • View Cell Size:这是另一个关键参数。它决定了将场景空间分割成的“细胞”大小。值越小,剔除精度越高,但烘焙数据量越大,运行时查询开销也略增。值越大,数据量小,但精度低,可能导致物体在应该出现时延迟出现(“ popping in”)。建议:对于室内场景或复杂城市,可以设为1-2米;对于广阔的野外地形,可以设为5-10米甚至更大。可以先使用一个较大的值(如5)快速烘焙测试,观察剔除效果,再逐步调小。
    • Save As:务必为烘焙结果指定一个名称并保存到项目中(如“SceneName_OcclusionData”)。否则数据不会持久化。

烘焙操作:点击Bake按钮。在Console窗口会看到进度。烘焙时间从几秒到几十分钟不等,取决于场景复杂度、参数设置和电脑性能。

4.4 第四步:验证与调试

烘焙完成不是终点,必须验证效果。

  1. 在Scene视图验证:在Occlusion Culling窗口,确保Visualization模式是打开的。在Scene视图中,你会看到被剔除的物体以红色线框显示(默认)。移动摄像机,观察红色物体的出现和消失是否符合逻辑。特别注意透明物体附近,看是否有不该消失的物体被错误剔除。
  2. 使用相机预览:在Game视图,选中主摄像机,在Inspector中找到Occlusion Culling选项并勾选。这样在Game视图也能看到剔除效果。同时,你可以启用摄像机的Occlusion Culling调试视图(通过脚本或插件),在运行时直观查看。
  3. 性能分析:使用Unity Profiler的Rendering模块,对比开启和关闭遮挡剔除时的Batches(批处理次数)和SetPass Calls的变化。一个有效的遮挡剔除应该能显著减少这些数值,尤其是在摄像机面向复杂区域时。

5. 常见问题排查与性能陷阱

即使按照上述流程操作,在实际项目中你还是可能会遇到各种奇怪的问题。下面是我踩过的一些坑以及解决方法。

5.1 问题一:烘焙后,物体在应该可见时被错误剔除(“穿帮”)

这是最严重的问题,直接导致画面错误。

可能原因及排查:

  1. 透明物体被误标为Occluder:这是头号嫌犯。检查所有透明物体(玻璃、水、粒子系统)的Static标记,确保Occluder Static没有被勾选。补救:立即取消勾选,并重新烘焙受影响区域(可以使用Bake按钮下的Clear后再Bake,或使用增量烘焙)。
  2. Occluder物体有“裂缝”或非闭合:如果一个应该作为遮挡的墙体模型本身有微小的裂缝或不是水密(watertight)的,遮挡计算可能会从裂缝中“泄漏”,导致其后的物体被意外剔除。排查:在3D建模软件中检查模型完整性,或尝试在Unity中为该物体添加一个简单的Box Collider,并临时用这个Collider代替Mesh作为Occluder进行烘焙测试(通过修改烘焙设置)。
  3. Smallest Hole参数过大:如果Occluder上存在比Smallest Hole参数更大的真实开口(如窗户),系统会错误地将其视为遮挡物的一部分,从而剔除窗后的物体。解决:减小Smallest Hole值,或更好的方法是,将带窗的墙体拆分成“窗框”(Occluder)和“窗洞”(空区域)两部分来处理。
  4. 物体缩放为负值或极端值:物体的Transform Scale如果为负或非常极端(如0.001),可能会导致其包围盒(Bounds)计算异常,进而影响遮挡计算。解决:规范资产导入和场景摆放,避免使用非均匀缩放或负值缩放。

5.2 问题二:遮挡剔除似乎没效果,性能没有提升

烘焙完成了,但Profiler里的Draw Calls并没减少。

可能原因及排查:

  1. 摄像机视锥体(Frustum)裁剪已足够:如果你的场景本身开阔,物体分布稀疏,大部分性能消耗可能已经在视锥体裁剪阶段被优化掉了,遮挡剔除的用武之地不大。这是正常现象,说明你的场景设计本身对渲染友好。
  2. Occludee Static未勾选:这是常见疏忽。很多物体只勾了Occluder,忘了勾Occludee。这意味着它们可以去挡别人,但自己永远不会被剔除。检查:批量检查中小型物体的Static设置。
  3. Smallest Occluder值设得过高:导致场景中有效的Occluder太少,没有足够的遮挡物去剔除后面的Occludee尝试:逐步调低此参数(如从50调到20),观察剔除效果的变化。注意平衡精度和烘焙成本。
  4. 动态物体过多:遮挡剔除只对标记为Occlusion Static的物体生效。如果你的场景中充满了动态移动的物体(NPC、可移动道具),它们无法被静态遮挡剔除优化。此时需要考虑其他优化手段,如动态遮挡剔除(如Unity的Occlusion Portal,但更常用于大型动态场景如MMO)、基于距离的LOD或简单的视距管理。

5.3 问题三:烘焙时间过长或数据文件巨大

可能原因及排查:

  1. 场景复杂度过高:面数太多、物体数量太多是根本原因。优化:在烘焙前,务必使用LOD Group为远处物体设置低模;合并(Merge)静态的、材质相同的小物体,减少Draw Calls和物体数量;检查是否有不必要的细分网格。
  2. View Cell Size过小:这是导致数据膨胀的主要原因。将View Cell Size从0.5提高到1.0,数据量可能减少一个数量级,而剔除精度在大多数情况下仍可接受。建议:永远从较大的View Cell Size开始测试。
  3. 过多细小物体被标记为Occluder:回顾我们的分类,大量“分类二”的物体如果错误标记了Occluder Static,会极大地增加烘焙复杂度。纠正:严格按照分类策略清理Static标记。
  4. 烘焙范围过大:检查场景中是否有位于很远处的、无关紧要的物体。可以适当调整烘焙的包围盒范围,或者将这些物体移到另一个场景中按需加载。

5.4 性能陷阱:过度使用Static的连锁反应

错误地滥用Static复选框,影响的远不止遮挡剔除。

  1. 静态批处理(Static Batching)的代价:当你勾选Static时,默认也会启用静态批处理。它会将多个静态、同材质的物体合并成一个大的网格进行绘制,减少Draw Calls。但是,这个合并过程会增加内存占用(存储合并后的网格),并且合并后的网格如果很大,可能会破坏GPU的视锥体裁剪效率(因为只要网格的任何一部分在视锥体内,整个大网格都会被提交渲染)。对于大量重复的小物体(如草地、碎石),静态批处理是福音;但对于少数几个巨大的独特静态物体,合并可能弊大于利。
  2. 导航网格(Navigation Static)的误用:如果你同时勾选了Navigation Static,这些物体会被纳入导航网格的烘焙计算。将大量细小装饰品或玩家根本不会走上去的物体(如吊灯)标记为可导航,会不必要地增加导航网格的复杂度和烘焙时间。
  3. 光照贴图(Lightmap Static)的牵连:同理,错误的Lightmap Static标记会导致光照贴图包含本不该包含的物体,增加光照烘焙时间和贴图尺寸。

最佳实践建议:不要无脑全选场景物体然后勾上Static。应该使用Static的下拉菜单,精确地、分门别类地勾选你需要的优化选项。例如,一个只用于遮挡的巨型背景山体,可能只需要勾选Occlusion Static,而不需要勾选Navigation StaticLightmap Static(如果它不受场景光照影响)。

← 返回列表