遮挡剔除Occlusion Culling和静态合批Batching
游戏中的元素非常多,但是摄像机能看到的内容是有限的,并且有些元素会被另外一些元素挡住。遮挡剔除顾名思义,摄像机看不见的,就剔除掉。
遮挡关系
例如一面墙后面放了很多元素,那么墙就属于遮挡物Occluder,墙后的物体就是被遮挡物Occludee。
我们在场景中放一些方体来做遮挡,在最前面放一个很大的做墙(注意一定要很大才行,unity默认情况下需要很大的Occluder),这些方体都要勾选Static Occluder和Static Occludee。
选择Window——Rendering——Occlusion Culling,打开烘焙面板,选择Bake选项卡,可以在里面设置最小遮挡距离,最小遮挡空隙以及背面的阈值,最后单击Bake按钮即可烘焙Static对象
烘焙结束后,Unity会在场景scene所在位置创建一个StaticObject文件夹,然后在其中放入OcclusionCullingData.asset文件,之前讲的场景烘焙文件也放在这里
然后,在scene面板中的Occlusion Culling小面板选择Visualize模式,在场景中就能实时查看剔除效果
Unity打开Occlusion面板后会不断地占用CPU和内存进行烘焙,所以烘焙完一定要关掉
遮挡剔除和锥形剔除是同时起效果的,具体可以查看官方手册
如果最前面的墙是透明的,那么墙后面的元素可以取消选择Occludee Static标志,如果最前面的墙是透明的,但是后面又是一块不透明的墙,那么后面的墙依然保留Occludee Static和Occluder Static勾选状态,实际应用中的思路便是如此。
遮挡与被遮挡事件
当发生遮挡剔除时,Unity会自动调用gameObject.SetActive(false)
方法,这样整个对象的渲染就会被暂停,直到它重新被启动。这时可以监听OnBecomeInvisible()
和OnBecomeVisible()
方法来处理一些逻辑
我们在之前的2D碰撞检测中使用过基于Unity事件系统的CollisionListener
和TriggerListener
用法,这里相似
新建OcclusionListener
脚本
1 | using UnityEngine; |
然后我们再使用下面的脚本来让游戏初始化时给所有的Render组件挂上OcclusionListener
脚本,统一监听它的剔除及显示方法。
如果想主动判断某个对象是否在摄像机显示区域内,也可以调用Renderer.isVisable()
方法。
新建AutoAddOcclusionListener
脚本
1 | using UnityEngine; |
动态剔除
任何游戏对象设置了Static后,在运行时都是禁止移动的。如果我们想要对象能被剔除又能够移动,我们可以设置动态剔除。
每一个Mesh Renderer的动态剔除都是默认开启的
勾选了动态剔除的对象,在Occluder Static静态物体后面,或者是位于摄像机视椎之外,都会被剔除。剔除只是关闭渲染,Update依然会更新。
在官方的文档中,关闭动态剔除可以用来制作墙后人物轮廓勾勒的特效。
自定义遮挡剔除
如果参与遮挡剔除的元素很多,每次移动摄像机时就会产生大量的计算,在移动平台更为明显。
我们可以自己实现遮挡剔除。比如,可以将场景上的元素按位置划分成若干个格子,每个格子里面就是场景中的游戏对象。无论游戏场景有多大,玩家同一时刻关心的只有1~9区域中的元素。当角色向左上方移动并超出当前格子的位置时,那么红色区域表示需要重新加载的,黄色区域表示需要保留的,蓝色区域表示需要释放的。
这可以保证最小化地管理所有游戏对象,而且这么做有个好处,当需要在主角范围内查找最近单元时,参与判断的对象如果很多,就会带来for循环判断的开销,但是由于我们只保留格子范围内的元素,判断就会非常快了。至于遮挡剔除,由于需要管理的对象已经很少了,遮挡剔除的优化几乎可以忽略,所以在移动摄像机的时候,就不会带来额外的开销了。
另外,还需要处理近景、远景的元素。比如近处的房子、树和草可能需要很快剔除掉,但是远处的城楼,山和云彩等不需要剔除,根据美术要求,将特殊元素过滤掉即可。
Bathing(静态合批)
模型是一个一个独立的,它们的材质以及贴图很有可能是完全一样的,但是由于模型不一样,放入Unity时就会多占很多的DrawCall,所以Unity提供了一个属性来做静态合批,可以将它们合并在一个Mesh里。
设置静态合批
静态合批首先需要在Player Settings——Other Settings勾选Static Bathing,想要动态合批也可以勾选Dynamic Bathing
然后在场景中选择需要合批的游戏对象,打开Bathing Static标记,然后运行游戏,Mesh Filter会自动生成一个新的Mesh,如果有相同的材质、Shader并且参数一致的话,就会合并DrawCall。
脚本静态合批
假如场景非常大,那么自动静态合并出来的Mesh就会非常大。运行游戏后,只要有一小部分出现在摄像机内,那么整个Mesh都要参与渲染。另外,静态合批的最大顶点数是65535,如果顶点数超过了它,Unity就会自动合并出多个多个Mesh。
我们可以利用脚本来动态设置需要合批在一起的游戏对象。注意,如果需要脚本合并,游戏对象不需要选中Static标记
新建CustomBatching
脚本
1 | using UnityEngine; |
要使用此脚本,需要一个根对象,然后将需要合批的游戏对象全部放在数组内
在应用了此脚本后,设置合批的对象就像设置Static标签一样不能移动,但是根对象(Root)是可以移动的
动态合批
Dynamic batching - Unity 手册 (unity3d.com)
动态合批是全自动的。但它是有要求的,
- Mesh的顶点数量需要小于300
- 如果Shader中使用了顶点位置、法线、UV0、UV1和切线,Mesh的顶点数必须小于180.
在Unity的Built-in粒子系统和Line Renderers、Trail Renderers组件中,动态合批都是自动开启的,可以优化很多DrawCall
静态合批的隐患
静态合批的原理是自动生成Mesh,但是不同Mesh保存的信息可能是不同的。例如Mesh中可能保存color和tangent,但是大部分Mesh是不需要这个信息的,如果静态合批中有一个Mesh包含了这个信息,那么合并以后整个Mesh都会带上它,这样无疑会增加一些额外的开销。
在3D软件中导出FBX时,可能会自动附带一些color信息,我们可以利用FBX官网提供的FBX接口,自己写一个Python脚本来删除它们
(书中只给提示,并未提供详细方法)