Unity什麼時候應該手動進行視域Culling?

Unity里的所有可視內容,都繼承至Renderer ("BillboardRenderer" "LineRenderer" "MeshRenderer" "ParticleRenderer" "ParticleSystemRenderer" "SkinnedMeshRenderer" "SpriteMask" "SpriteRenderer" "TilemapRenderer" "TrailRenderer"),而Unity每次渲染實際上是從四叉樹里篩選出全部的Renderer進行處理的,這個操作的效率和屏幕外有多少不顯示的Renderer無關,所以強行把屏幕外側的Renderer禁用對提升效率並沒有實質意義,反而會在禁用和啟用時有多餘的計算。

但是,這也是在Cull生效的前提下。你選擇不Cull,它自然就不會Cull。

而以上大部分的Renderer都提供了是否Cull的選項,而且都是默認是激活的(而MeshRenderer和SpriteRenderer則是想關掉Cull都做不到)。

那麼,在激活自動Cull的情況下,手動Cull是否就是沒有必要的呢?

並不是,因為ParticleSystem存在例外。

準確的說,Renderer渲染部分還是會正常Cull的,但是在某些特殊情況下,粒子的計算將會永久持續進行,而大家顯然更在意的是後者。

1.World粒子

2.與碰撞體的交互

3.子發射器

4.噪波

以及其他。

具體還有哪些我也說不全,總之,一旦出現無法Cull的情況,粒子的界面上就會出現這樣一個感嘆號提示

而出現這種狀況的原因是:

Unity的粒子其實有兩種模式,一種會根據上一幀的粒子信息計算當前幀信息,而另一種,實際上每幀的粒子位置都是通過曲線公式重新求出的。

當粒子出屏被Cull,過一段時間再進入屏幕的時候,由於後者是根據時間重新計算的粒子坐標,取當前時間就能顯示出正確的結果,但前者在出屏的那段時間就相當於時間靜止了。這對於循環特效或許不是問題,對於一次性特效就會出現明顯的錯誤。

其實在早期的Unity版本里,如果你用World粒子做過腳印就會發現,當其他人遠離你很長時間後,你再追過去,依然可以看到他很早以前留下的腳印,這就是因為World空間粒子在出屏後時間靜止了。當時我只能選擇用巨大的bounds讓腳印粒子永遠不被Cull。

而現在,Unity就直接選擇讓這種粒子在任何情況下都必須計算了,而且,不給我們拒絕的選項。

總之,凡是不能通過公式直接推算出任意時間狀態的粒子,都會出現這個感嘆號,然後禁用自動Cull。這個規則是很難給美術說清楚的,而且,該用的東西,即使不能Cull也必須要用。

所以,就結果而言,粒子系統必須做手動Cull。

除了粒子之外,還有一種情況是SkinMeshRenderer。SkinMeshRenderer默認情況是用面板上設置的固定Bounds來進行Cull的,而這個默認Bounds是根據原始Mesh的數據來決定的,是一個固定值,和動作無關,所以模型如果做出一些類似「橡膠手槍」的動作,超出Bounds範圍,就會出現Cull錯誤。

想解決這個問題,比較方便的做法是在做這些動作的時候設置updateWhenOffscreen為true(粒子系統也給這樣一個選項就好了),這樣它就會動態地計算bounds,可以讓Renderer被正確裁剪並減少多邊形和DC,但同時,在任何時候都要計算蒙皮,而且聯帶著導致Animation系統也必須始終計算。

這裡順帶提到了Animation/Animator,Animation默認是不Cull的,Animator默認則是會計算動畫,但不更新骨骼節點,和不Cull也沒啥區別。雖然他們都可以設置成Base On Renderer,但同時就會出現出屏後的Stop The World問題。

(因為bounds計算是以骨骼根節點為準的,所以角色做一個高跳出屏幕的動作的時候,由於跳出了屏幕,動作就被cull了,然後就回不來了,這也是實際工作中常見的問題,擺動幅度大的動作激活cull是很危險的)

總之都很麻煩。我之前的做法是,讓人物在做出loop動作時自動Cull(靜止or移動),而做出非loop技能動作的時候則不Cull,基本能解決問題。

但假如你選擇手動Cull,粗略處理的話……確實就可以不用管這些事了。

所以,手動Cull主要針對的是粒子和蒙皮人物。然而,假如你希望做「遠離屏幕的物體自動銷毀進池並根據情況回收內存這種操作」的話……手動Cull就更有必要去做了。

但是既然要Cull,就要做得精確。直接偏移攝像機定一個點然後測算距離是非常粗暴,以及「不負責任」的做法,容易出現該剔除的不剔除,或者剔除不及時的情況,要不就是不該剔除的反而剔除的情況(好多遊戲都有出現)

但精確計算,計算量就會增大,就不好再粗暴地用全部物體掛腳本每幀計算的方式了(當然即使沒這情況也不建議這麼做,空轉的Update其實比你想像的更費性能)。Cull計算應該局限在視野內,與視野外物體數量不相關,所以最佳方案是用四叉樹篩選。然而這個準備工作明明Unity已經做了一次了,我們再做一次是重複勞動。因此,理想的做法就是設法去利用Unity自己的Cull系統。

王者榮耀對於粒子Cull的處理,就是在粒子根節點放置一個空的MeshRenderer&MeshFilter,加入一個空Mesh並設置Bounds,然後再加一個自定義腳本接受OnBecameVisible和OnBecameInvisible,用這個事件禁用內部的粒子組件。

看起來很蠢?然而在之前的舊版本里,這就是最優解。至少這種做法效率比起加個腳本一直Update要高,假如還要精確計算Bounds和視錐相交,就更沒法比了。

新版本的Unity則出了一個CullingGroup來解決這個問題,它本身和Unity自己的Cull系統以及LodGroup是同一體系,相當於開放了一些Cull底層的功能供用戶使用。

但要注意的是,這些Cull檢測其實都是多餘的性能消耗,依然還是能不用就不用。負優化即使少,也是不應該容忍的。

CullingGroup的具體內容可參考官方幫助,下面是些現成輪子。

單個物體CullingGroup

using UnityEngine;public class SingleCulling : MonoBehaviour{ public float cullingRadius = 1; public Vector3 offest; public Camera targetCamera; private CullingGroup mCullingGroup; void Awake() { mCullingGroup = new CullingGroup(); mCullingGroup.SetBoundingSphereCount(1); mCullingGroup.onStateChanged += OnStateChanged; } void Start() { UpdateCullingGroup(); } public bool IsVisible() { return mCullingGroup.IsVisible(0); } public void UpdateCullingGroup() { if (mCullingGroup == null) return; mCullingGroup.targetCamera = targetCamera ? targetCamera : Camera.main; mCullingGroup.SetBoundingSpheres(new BoundingSphere[] { new BoundingSphere(transform.TransformPoint(offest), cullingRadius) }); } void OnStateChanged(CullingGroupEvent sphere) { if (sphere.isVisible) OnBecameVisible(); else OnBecameInvisible(); } protected virtual void OnBecameVisible() { } protected virtual void OnBecameInvisible() { } void OnDestroy() { if (mCullingGroup != null) mCullingGroup.Dispose(); }#if UNITY_EDITOR void OnDrawGizmosSelected() { Gizmos.color = Color.grey; Gizmos.DrawWireSphere(transform.TransformPoint(offest), cullingRadius); } private void OnValidate() { UpdateCullingGroup(); }#endif}

多個物體CullingGroup

using UnityEngine;using System;using System.Collections.Generic;public class MultipleCulling : MonoBehaviour{ public float cullingRadius = 1; public Vector3 offest; public int index { get; internal set; } public MultipleCullingGroup parent { get; internal set; } public bool IsVisible() { return parent.IsVisible(this); } public BoundingSphere GetBoundingSphere() { return new BoundingSphere(transform.TransformPoint(offest), cullingRadius); } internal virtual void OnBecameVisible() { } internal virtual void OnBecameInvisible() { }#if UNITY_EDITOR void OnDrawGizmosSelected() { Gizmos.color = Color.grey; Gizmos.DrawWireSphere(transform.TransformPoint(offest), cullingRadius); }#endif}public class MultipleCullingGroup : IDisposable{ CullingGroup cullingGroup; MultipleCulling[] cullingObjects; BoundingSphere[] spheres; public MultipleCullingGroup() { cullingGroup = new CullingGroup(); cullingGroup.targetCamera = Camera.main; cullingGroup.onStateChanged += OnStateChanged; } Camera targetCamera { get { return cullingGroup.targetCamera; } set { cullingGroup.targetCamera = value; } } void OnStateChanged(CullingGroupEvent sphere) { if (sphere.isVisible) cullingObjects[sphere.index].OnBecameVisible(); else cullingObjects[sphere.index].OnBecameInvisible(); } public void AddCullingObjects(MultipleCulling[] objects) { int oldLen = cullingObjects != null ? cullingObjects.Length : 0; int newLen = oldLen + objects.Length; if (cullingObjects != null) Array.Resize<MultipleCulling>(ref cullingObjects, newLen); else cullingObjects = new MultipleCulling[newLen]; if (spheres != null) Array.Resize<BoundingSphere>(ref spheres, newLen); else spheres = new BoundingSphere[newLen]; for (int i = oldLen; i < newLen; i++) { MultipleCulling cull = objects[i - oldLen]; cull.index = i; cull.parent = this; cullingObjects[i] = cull; spheres[i] = cull.GetBoundingSphere(); } cullingGroup.SetBoundingSpheres(spheres); cullingGroup.SetBoundingSphereCount(newLen); } public void RemoveCullingObjects(MultipleCulling[] objects) { if (cullingObjects == null) return; HashSet<MultipleCulling> hashSet = new HashSet<MultipleCulling>(objects); int len = cullingObjects.Length; int newLen = len; for (int i = 0; i < len;i++) { if (hashSet.Contains(cullingObjects[i])) { cullingObjects[i] = null; newLen--; } } MultipleCulling[] newCullingObjects = new MultipleCulling[newLen]; BoundingSphere[] newSpheres = new BoundingSphere[newLen]; int newIndex = 0; for (int i = 0; i < len;i++) { if (cullingObjects[i] != null) { MultipleCulling cull = cullingObjects[i]; cull.index = newIndex; newCullingObjects[newIndex] = cull; newSpheres[newIndex] = spheres[i]; newIndex++; } } cullingObjects = newCullingObjects; spheres = newSpheres; cullingGroup.SetBoundingSpheres(spheres); cullingGroup.SetBoundingSphereCount(newLen); } public void UpdateCullingObjects(MultipleCulling[] objects) { foreach (MultipleCulling cull in objects) { spheres[cull.index] = cull.GetBoundingSphere(); } cullingGroup.SetBoundingSpheres(spheres); } public void ClearCullingObjects() { cullingObjects = null; spheres = null; cullingGroup.SetBoundingSpheres(new BoundingSphere[0]); cullingGroup.SetBoundingSphereCount(0); } public bool IsVisible(MultipleCulling sphece) { return cullingGroup.IsVisible(sphece.index); } public void Dispose() { if (cullingGroup != null) { cullingGroup.Dispose(); cullingGroup = null; } }}

最後這是個檢測粒子能否能自動Cull的編輯器腳本,可以根據這個自動添加Cull腳本

using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEditor;using System;using System.Reflection;namespace UnityEditor{ public class CheckFailParticleCulling { [MenuItem("Tools/Paritex", false)] static public void Check() { Assembly ass = typeof(UnityEditor.Editor).Assembly; Type t = ass.GetType("UnityEditor.ParticleSystemUI"); MethodInfo init = t.GetMethod("Init"); FieldInfo m_SupportsCullingText = t.GetField("m_SupportsCullingText", BindingFlags.Instance | BindingFlags.NonPublic); object particleSystemUI = ass.CreateInstance("UnityEditor.ParticleSystemUI"); foreach (var obj in Selection.objects) { if (obj is GameObject) { ParticleSystem[] particles = (obj as GameObject).GetComponentsInChildren<ParticleSystem>(true); string result = ""; foreach (ParticleSystem particle in particles) { init.Invoke(particleSystemUI, new object[] { null, new ParticleSystem[] { particle } }); string subResult = m_SupportsCullingText.GetValue(particleSystemUI) as string; if (subResult != null) { result += particle.name + " (" + subResult.Replace("
","") + ")
"; } } if (result != "") { Debug.Log(AssetDatabase.GetAssetPath(obj) + "
" + result,obj); } } } } }}

推薦閱讀:

Billboards 技術在Unity 中的幾種使用方法
300行代碼實現Minecraft(我的世界)大地圖生成
【Unity】工具類系列教程—— 代碼自動化生成!
幻影坦克架構指南(二)
GPU Gems 基於物理模型的水面模擬 學習筆記 (一)

TAG:Unity游戏引擎 |