文章目录
环境
Unity : 2019.4.0f1
都是很久之前写过的内容,但是写了一般,还有一些 BUG,后续会回头来完善
前一篇:有讲到 QuadTree 四叉树的剔除DEMO:Unity - 撸一个简单版本的 四叉树 + 视锥cascaded,用于场景剔除
那么这篇就是粗略演示如何使用
如果可以的话,可以将 四叉树 修改 为 八叉树,就会更加适用于 3D 场景的应用
场景简述
- XXX_Doing 的是原始场景
- XXX_Exported 都是在 Doing 场景基础上提取并替换了 GameObject 到 Instancing 数据后导出的
所以我们只要运行 Exported 场景就可以看到效果
示例
以一个放了3.5 W 棵树左右的场景来测试,如下图,这些顶点数据都有 1000W+ 的数量了
然后用一个自己写的工具提取可以 GPU Instancing 的GameObject的信息
Instancing 组件
效果
Code
ExportGpuInstancingSceneWindow.cs
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ExportGpuInstancingSceneWindow : EditorWindow
{
// 原始场景路径
private string src_scene_path = "";
// instancing threshold - 需要 instancing 的实例阈值
private int MIN_INST_COUNT_NEW = 1;
private int instancing_count_threshold = 20;
// 当前需要被处理的场景
private Scene cur_src_scene;
// 分析出来的配置数据
private InstancingAllCfgInfo cfgInfo = null;
private string output_cfg_path = "Assets/Resources/Cfgs/{0}_InstancingAllCfgInfo.asset";
private bool is_clear_before_same_type_data = true;
private Object export_scene_selected_go;
[MenuItem("Tools/导出 GPU Instancing 场景配置工具")]
public static void _Show()
{
EditorWindow.GetWindow<ExportGpuInstancingSceneWindow>().Show();
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Src Scene Name : ");
src_scene_path = EditorGUILayout.TextField(src_scene_path);
if (GUILayout.Button("设置选中的场景"))
{
var scene_go = Selection.activeObject;
var asset_path = AssetDatabase.GetAssetPath(scene_go);
if (Path.GetExtension(asset_path).ToLower() != ".unity")
{
EditorUtility.DisplayDialog("提示", "选中的不是场景", "确定");
}
else
{
src_scene_path = asset_path;
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Instancing Count Tthreshold : ");
instancing_count_threshold = EditorGUILayout.IntField(instancing_count_threshold);
EditorGUILayout.EndHorizontal();
if (instancing_count_threshold < MIN_INST_COUNT_NEW)
{
instancing_count_threshold = MIN_INST_COUNT_NEW;
}
if (GUILayout.Button("分析[整个场景]的 可 Instancing 数据(注意会重新导出整个场景,覆盖原来导出的)"))
{
if (EditorUtility.DisplayDialog("提示", "确定分析[整个]场景的 可 Instancing 数据吗(时间比较长)?", "确定", "取消"))
{
AnalyseCanInstancingByWholeScene();
}
}
EditorGUILayout.LabelField("---------------------------------");
export_scene_selected_go = EditorGUILayout.ObjectField("选中的 Instancing Root 对象", export_scene_selected_go, typeof(UnityEngine.Object), true);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("分析[选中对象]的 可 Instancing 数据(在原有的场景的基础上做提取和删除GO)"))
{
//var selected_go = Selection.activeObject as GameObject;
var selected_go = export_scene_selected_go as GameObject;
if (selected_go == null)
{
EditorUtility.DisplayDialog("提示", "选中的对象不是继承自 GameObject!", "确定");
}
else
{
if (!SelectedGoIsInSeceneObjs(selected_go))
{
EditorUtility.DisplayDialog("提示", "选中的对象当前场景的 GameObject 对象!", "确定");
}
else
{
AnalyseCanInstancingBySelectedGo(selected_go);
}
}
}
is_clear_before_same_type_data = GUILayout.Toggle(is_clear_before_same_type_data, "是否删除之前同类型的数据");
EditorGUILayout.EndHorizontal();
// 显示可 instancing 的数据
if (cfgInfo != null && cfgInfo.list.Count > 0)
{
foreach (var info in cfgInfo.list)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"{info.sharedMeshPath}_{info.sharedMaterialPath}:{info.matrixWholeArray.Length}");
info.export = EditorGUILayout.Toggle(info.export);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.LabelField("---------------------------------");
// 导出
if (GUILayout.Button("导出 可 Instancing 数据"))
{
ExportToCfgInfo();
}
EditorGUILayout.LabelField("---------------------------------");
}
}
private bool SelectedGoIsInSeceneObjs(GameObject go)
{
if (!cur_src_scene.IsValid() || cur_src_scene.path != src_scene_path)
{
cur_src_scene = EditorSceneManager.OpenScene(src_scene_path, OpenSceneMode.Single);
if (!cur_src_scene.IsValid())
{
return false;
}
}
// 提取新场景 instancing 的数据,并删除对应的 go
var gos = cur_src_scene.GetRootGameObjects();
cfgInfo = ScriptableObject.CreateInstance<InstancingAllCfgInfo>();
cfgInfo.cam_path = "Main Camera";
// 提取需要 instancing 数据
foreach (var search_go in gos)
{
if (_SelectedGoIsInSeceneObjs(go, search_go))
{
return true;
}
}
return false;
}
private bool _SelectedGoIsInSeceneObjs(GameObject go, GameObject search_go)
{
if (go == null || search_go == null)
{
return false;
}
if (go == search_go)
{
return true;
}
var count = search_go.transform.childCount;
for (int i = 0; i < count; i++)
{
var trans = search_go.transform.GetChild(i);
if (_SelectedGoIsInSeceneObjs(go, trans.gameObject))
{
return true;
}
}
return false;
}
private StringBuilder strBuilderHelper = new StringBuilder();
private string GetHierarchyPath(GameObject go)
{
strBuilderHelper.Clear();
while (go != null)
{
strBuilderHelper.Append($"/{go.name}");
go = go.transform.parent != null ? go.transform.parent.gameObject : null;
}
return strBuilderHelper.ToString();
}
// 根据整个场景下的所有 GameObject 对象来提取可以 Instancing 的数据提取
private void AnalyseCanInstancingByWholeScene()
{
if (string.IsNullOrEmpty(src_scene_path))
{
Debug.LogError($"{nameof(ExportGpuInstancingSceneWindow)}.{nameof(AnalyseCanInstancingByWholeScene)}, {nameof(src_scene_path)} is empty or null.");
return;
}
cur_src_scene = EditorSceneManager.OpenScene(src_scene_path, OpenSceneMode.Single);
if (!cur_src_scene.IsValid())
{
Debug.LogError($"{nameof(ExportGpuInstancingSceneWindow)}.{nameof(AnalyseCanInstancingByWholeScene)}, {nameof(src_scene_path)}:{src_scene_path} according sceneObj maybe not exsit.");
return;
}
// 新场景名字
var pure_file_name = Path.GetFileNameWithoutExtension(src_scene_path);
var export_scene_path = src_scene_path.Replace(pure_file_name, pure_file_name + "_Exported");
// 提取新场景 instancing 的数据,并删除对应的 go
var gos = cur_src_scene.GetRootGameObjects();
cfgInfo = ScriptableObject.CreateInstance<InstancingAllCfgInfo>();
cfgInfo.cam_path = "Main Camera";
// 提取需要 instancing 数据
foreach (var go in gos)
{
PickupGoChildrenAllCanInstancingData(go);
}
// 提取后的再次分析
FilterByInstancingCountThreshold();
}
// 根据选中的 GameObject 对象来提取可以 Instancing 的数据提取
private void AnalyseCanInstancingBySelectedGo(GameObject go)
{
if (string.IsNullOrEmpty(src_scene_path))
{
Debug.LogError($"{nameof(ExportGpuInstancingSceneWindow)}.{nameof(AnalyseCanInstancingByWholeScene)}, {nameof(src_scene_path)} is empty or null.");
return;
}
if (!cur_src_scene.IsValid() || cur_src_scene.path != src_scene_path)
{
cur_src_scene = EditorSceneManager.OpenScene(src_scene_path, OpenSceneMode.Single);
if (!cur_src_scene.IsValid())
{
Debug.LogError($"{nameof(ExportGpuInstancingSceneWindow)}.{nameof(AnalyseCanInstancingByWholeScene)}, {nameof(src_scene_path)}:{src_scene_path} according sceneObj maybe not exsit.");
}
}
// 新场景名字
var pure_file_name = Path.GetFileNameWithoutExtension(src_scene_path);
var export_scene_path = src_scene_path.Replace(pure_file_name, pure_file_name + "_Exported");
if (cfgInfo == null)
{
var output_path = string.Format(output_cfg_path, pure_file_name);
cfgInfo = AssetDatabase.LoadAssetAtPath<InstancingAllCfgInfo>(output_path);
if (cfgInfo == null)
{
cfgInfo = ScriptableObject.CreateInstance<InstancingAllCfgInfo>();
}
cfgInfo.cam_path = "Main Camera";
}
// 提取需要 instancing 数据
PickupGoChildrenAllCanInstancingData(go);
// 提取后的再次分析
FilterByInstancingCountThreshold();
}
// 提取对应 GameObject 下的所有可以 Instancing 的数据
private void PickupGoChildrenAllCanInstancingData(GameObject go)
{
// 这里根据是否需要删除之前同类型的提取数据来处理
Dictionary<string, bool> need_to_clear_flag_dict = null;
if (is_clear_before_same_type_data)
{
need_to_clear_flag_dict = new Dictionary<string, bool>();
}
// test start
HashSet<Bounds> distinctSet = new HashSet<Bounds>();
// test end
var renderers = go.GetComponentsInChildren<MeshRenderer>(true);
if (renderers != null && renderers.Length > 0)
{
foreach (var renderer in renderers)
{
if (renderer == null)
{
Debug.LogError($"{nameof(src_scene_path)}: hierarchy : {GetHierarchyPath(renderer.gameObject)} not found {nameof(MeshRenderer)} Component!");
continue;
}
var filter = renderer.gameObject.GetComponent<MeshFilter>();
if (filter == null)
{
Debug.LogError($"{nameof(src_scene_path)}: hierarchy : {GetHierarchyPath(renderer.gameObject)} not found {nameof(MeshFilter)} Component!");
continue;
}
// shared mesh
var sharedMesh = filter.sharedMesh;
// shared mesh path
var sharedMeshPath = AssetDatabase.GetAssetPath(sharedMesh);
Debug.Log($"Trying pick up the path of shared mesh : {sharedMeshPath}");
if (sharedMeshPath.StartsWith("Library"))
{
Debug.LogWarning($"{nameof(src_scene_path)}: hierarchy : {GetHierarchyPath(renderer.gameObject)} mesh is built-in asset : {sharedMeshPath}!");
continue;
}
// shared materials
var sharedMaterial = renderer.sharedMaterial;
var sharedMaterialPath = AssetDatabase.GetAssetPath(sharedMaterial);
Debug.Log($"Trying pick up the path of shared material : {sharedMaterialPath}");
if (sharedMaterialPath.StartsWith("Library"))
{
Debug.LogError($"{nameof(src_scene_path)}: hierarchy : {GetHierarchyPath(renderer.gameObject)} mesh is built-in asset : {sharedMaterialPath}!");
continue;
}
var key = sharedMeshPath + "_" + sharedMaterialPath;
if (is_clear_before_same_type_data && !need_to_clear_flag_dict.ContainsKey(key))
{
cfgInfo.instancingDict.Remove(key);
need_to_clear_flag_dict[key] = true;
}
if (!cfgInfo.instancingDict.TryGetValue(key, out InstancingSingleBatchingInfos info))
{
info = new InstancingSingleBatchingInfos();
// 网格、材质的路径
info.sharedMeshPath = sharedMeshPath;
info.sharedMaterialPath = sharedMaterialPath;
// editor 模式下的网格、材质对象的查看,便于点击索引到 Project 视图中快速定位
info.sharedMeshObj = sharedMesh;
info.sharedMaterialObj = sharedMaterial;
cfgInfo.instancingDict[key] = info;
}
// bounds
if (distinctSet.Contains(renderer.bounds))
{
Debug.LogError($"有重复的位置的树添加进来~, renderer.gameObject.name : {renderer.gameObject.name}");
}
else
{
info.AddBounds(renderer.bounds);
distinctSet.Add(renderer.bounds);
}
// for editor, if instancing, fill destroy according GameObject
info.gameObjs.Add(renderer.gameObject);
// 提取每个 instancing 的变换数据
// model matrix
info.matrixList4Editing.Add(renderer.transform.localToWorldMatrix);
}
}
}
// 再次使用 GPU Instancing 数量的阈值来过滤一波
private void FilterByInstancingCountThreshold()
{
// 删除 低于阈值实例数量的数据
var un_neccessary_instancing_removeIDs = new List<string>();
foreach (var kv in cfgInfo.instancingDict)
{
if (kv.Value.matrixList4Editing.Count < instancing_count_threshold)
{
un_neccessary_instancing_removeIDs.Add(kv.Key);
}
}
foreach (var id in un_neccessary_instancing_removeIDs)
{
cfgInfo.instancingDict.Remove(id);
}
// 剩下的 cfg info 才添加到 list
foreach (var kv in cfgInfo.instancingDict)
{
var info = kv.Value;
cfgInfo.list.Add(info);
info.matrixWholeArray = info.matrixList4Editing.ToArray();
info.matrixList4Editing.Clear();
}
}
// 保存要导出的 Instancing 配置数据
private void ExportToCfgInfo()
{
if (cfgInfo == null || cfgInfo.list.Count == 0)
{
Debug.Log("have no Instancing Cfg Info");
return;
}
if (!this.cur_src_scene.IsValid())
{
Debug.LogError($"{(nameof(ExportGpuInstancingSceneWindow))}.{nameof(AnalyseCanInstancingByWholeScene)}, {nameof(src_scene_path)}:{src_scene_path} according sceneObj maybe not exsit.");
return;
}
// 新场景名字
var pure_file_name = Path.GetFileNameWithoutExtension(src_scene_path);
var export_scene_path = src_scene_path.Replace(pure_file_name, pure_file_name + "_Exported");
AssetDatabase.DeleteAsset(export_scene_path);
// grass instancing go mgr
var grass_istancing_go_mgr = new GameObject($"GrassInstancingGoMgr");
var instancingMgrCom = grass_istancing_go_mgr.AddComponent<InstancingMgrCom>();
// draw camera 就不设置了,因为 camera 最好在运行时动态设置对应的
// 当然也可以为了测试用,这里可以写一个默认的
instancingMgrCom.cfg = cfgInfo;
instancingMgrCom.cfg.cam_path = "Main Camera";
// 保存 instancing infos 到 scriptable object 配置
// 需要 instancing 的,就将对应的 go 删除
// 计算 while bounds
for (int i = cfgInfo.list.Count - 1; i > -1; i--)
{
var info = cfgInfo.list[i];
if (info.export)
{
// 如果需要导出 istancing 配置信息
foreach (var go in info.gameObjs)
{
GameObject.DestroyImmediate(go);
}
info.gameObjs.Clear();
var whileBounds = info.boundsList[0];
whileBounds.w = 0;
whileBounds.h = 0;
foreach (var bounds in info.boundsList)
{
whileBounds.Union(bounds);
}
info.UpdateWhileBounds(whileBounds);
}
else
{
// 如果不需要导出,就从提取到的配置中删除
cfgInfo.list.RemoveAt(i);
}
}
var output_path = string.Format(output_cfg_path, pure_file_name);
AssetDatabase.DeleteAsset(output_path);
AssetDatabase.CreateAsset(cfgInfo, output_path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 另存导出的场景
EditorSceneManager.SaveScene(cur_src_scene, export_scene_path, true);
// 最后重新打开这个导出后的另存场景
EditorSceneManager.OpenScene(export_scene_path, OpenSceneMode.Single);
}
}
public class ExportGpuInstancingObjScene
{
[MenuItem("Tools/ExportSceneGrass")]
public static void ExportGpuInstancingObjSceneEntry()
{
var sceneObj = SceneManager.GetSceneByName("SampleScene");
var gos = sceneObj.GetRootGameObjects();
foreach (var go in gos)
{
var coms = go.GetComponentsInChildren<Instancing>(true);
if (coms != null)
{
foreach (var com in coms)
{
if (com.gameObject.name.Equals("TestingQuad"))
{
//GameObject.Destroy(com);
GameObject.DestroyImmediate(com);
Debug.LogError($"Destroy TestingQuad Obj.");
}
else
{
Debug.Log($"Find the grass instancing, name : {com.name}.");
}
}
}
}
//var PlaneTrans = sceneObj.transform.Find("Plane");
//Debug.LogError($"PlaneTrans.name : {PlaneTrans.name}");
//var copy_scene = SceneManager.CreateScene("SampleScene_Exported");
//SceneManager.MergeScenes(sceneObj, copy_scene);
}
}
InstancingMgrCom.cs
using QuadTree;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEditor;
using UnityEngine;
#if UNITY_EDITOR
public enum eDrawType
{
T1_DRAW_INST_PLUS_CS,
T2_DRAW_INST,
T3_DRAW_MESH,
}
#endif
public class InstancingMgrCom : MonoBehaviour
{
public const int NORMAL_API_INST_MAX_COUNT = 1023;
[Header("绘制镜头的 aabb 的颜色")]
public Color cam_aabb_color = Color.red;
[Header("绘制镜头视锥的 aabb 的颜色")]
public Color cam_frustum_color = Color.yellow;
[Header("绘制四叉树的枝干 aabb 的颜色")]
public Color qt_branches_color = Color.cyan;
[Header("绘制四叉树的叶子 aabb 的颜色")]
public Color qt_leaves_color = Color.green;
[Header("绘制镜头视锥内的 aabb 四叉树的叶子的 aabb 的颜色")]
public Color qt_leaves_in_frustum_color = Color.blue;
[Header("绘制四叉树的 culling distance 半径的颜色")]
public Color qt_culling_distance_color = Color.black;
[Header("是否绘制镜头的 aabb")]
public bool draw_gizmos_cam_aabb = true;
[Header("是否绘制镜头视锥的 wireframe")]
public bool draw_gizmos_cam_wireframe = true;
[Header("是否绘制四叉树的枝干 aabb")]
public bool draw_gizmos_qt_branches = true;
[Header("是否绘制四叉树的叶子 aabb")]
public bool draw_gizmos_qt_leaves = true;
[Header("是否绘制镜头视锥内的 aabb 四叉树的叶子的 aabb")]
public bool draw_gizmos_qt_leaves_in_frustum_aabb = true;
[Header("是否绘制四叉树的 cullingDistancing 半径")]
public bool draw_gizmos_qt_cullingDistancing = true;
[Header("视锥分段的多层 AABB 的级别")]
[Range(1, 20)]
public int frustum_AABB_level = 3;
[Header("视锥水平剔除空间扩大的 unit 单位")]
public float frustum_h_padding = 5;
[Header("开启的话,结果会再精准一些,但会增加部分计算量(也可以关闭后,让外部来处理:稍微精确一些的筛选)")]
public bool more_actually_select = true;
#if UNITY_EDITOR
[Header("当前的绘制方式")]
public eDrawType draw_type = eDrawType.T1_DRAW_INST_PLUS_CS;
#endif
[Header("超出此距离的 aabb 都会被剔除掉(粗粒度剔除)")]
public float cullingDistance = 600;
public InstancingAllCfgInfo cfg;
private Dictionary<string, Camera> cached_camera = new Dictionary<string, Camera>();
private Dictionary<string, Mesh> cached_mesh = new Dictionary<string, Mesh>();
private Dictionary<string, Material> cached_material = new Dictionary<string, Material>();
private List<QTAABB> cam_aabbs = new List<QTAABB>();
private void Start()
{
Debug.Log($"{nameof(InstancingMgrCom)} support Instance : {SystemInfo.supportsInstancing}, supports ComputeShader : {SystemInfo.supportsComputeShaders}");
// 先使用 八叉树,或是 BVH,BSP,来讲对应的 bounds 放到对应的空间,加速后续的 LateUpdate 中的绘制前剔除
// code here
// 构建四叉树
foreach (var info in cfg.list)
{
info.matrixArray4Drawing = new Matrix4x4[info.matrixWholeArray.Length];
info.matrixArray4Culling = new Matrix4x4[info.matrixWholeArray.Length];
var qt = new QuadTree<int>(info.whileBounds, 10, 50);
for (int k = 0; k < info.boundsList.Count; k++)
{
qt.Insert(k, info.boundsList[k]);
}
info.quadtree = qt;
}
}
private void OnDestroy()
{
stop_culling_in_thread = false;
if (qt_culling_thread != null)
{
try
{
qt_culling_thread.Abort();
}
catch
{
}
qt_culling_thread = null;
}
}
private void LateUpdate()
{
if (cfg == null)
{
return;
}
_LateDraw();
}
private void _LateDraw()
{
// 根据相机剔除草对象
if (!cached_camera.TryGetValue(cfg.cam_path, out Camera drawCam))
{
drawCam = GameObject.Find(cfg.cam_path).GetComponent<Camera>();
cached_camera[cfg.cam_path] = drawCam;
}
var src_oc = drawCam.useOcclusionCulling;
drawCam.useOcclusionCulling = true;
foreach (var inst in cfg.list)
{
if (!cached_mesh.TryGetValue(inst.sharedMeshPath, out Mesh mesh))
{
#if UNITY_EDITOR
mesh = AssetDatabase.LoadAssetAtPath<Mesh>(inst.sharedMeshPath);
#else
var path = inst.sharedMeshPath.Replace("Assets/Resources/", "");
if (path.Contains("."))
{
path = path.Substring(0, path.LastIndexOf("."));
}
mesh = Resources.Load<Mesh>(path);
#endif
}
if (!cached_material.TryGetValue(inst.sharedMaterialPath, out Material material))
{
#if UNITY_EDITOR
material = AssetDatabase.LoadAssetAtPath<Material>(inst.sharedMaterialPath);
#else
var path = inst.sharedMaterialPath.Replace("Assets/Resources/", "");
if (path.Contains("."))
{
path = path.Substring(0, path.LastIndexOf("."));
}
material = Resources.Load<Material>(path);
#endif
}
// 开始绘制
if (inst.matPropBlock == null)
{
inst.matPropBlock = new MaterialPropertyBlock();
}
_Draw(drawCam, inst, mesh, material);
}
drawCam.useOcclusionCulling = src_oc;
}
private void _Draw(Camera drawCam, InstancingSingleBatchingInfos info, Mesh mesh, Material material)
{
if (
//false &&
SystemInfo.supportsInstancing)
{
if (
false && // 目前没有加入 HiZ,暂时屏蔽
SystemInfo.supportsComputeShaders)
{
// instanced indirect + HiZ
// Graphics.DrawMeshInstancedIndirect()
// 这里可以走:GPUInstancer 中的逻辑来处理
// 但是不打算用他这个插件的整个功能
#if UNITY_EDITOR
draw_type = eDrawType.T1_DRAW_INST_PLUS_CS;
#endif
}
else // just draw instanced
{
#if UNITY_EDITOR
draw_type = eDrawType.T2_DRAW_INST;
#endif
// 先使用最简单的
// culling
UpdateValidatedDrawCount(info, drawCam, more_actually_select);
// unity draw instanced 会限制一次最多 1023 个对象
var idx = 0;
for (int i = 0; i < info.validatedInstCount; i += NORMAL_API_INST_MAX_COUNT)
{
var start = idx * NORMAL_API_INST_MAX_COUNT;
var end = (idx + 1) * NORMAL_API_INST_MAX_COUNT;
end = Mathf.Min(end, info.validatedInstCount);
var draw_count = end - start;
// 因为 Graphics.DrawMeshInstanced 没得支持设置 matrix array start index
// 所以导致这里只能每次分批绘制时,只能自己去弄另一个 array 来绘制
Array.Copy(info.matrixArray4Culling, start, info.matrixArray4Drawing, 0, draw_count);
Graphics.DrawMeshInstanced(
mesh, 0,
material,
info.matrixArray4Drawing,
draw_count,
info.matPropBlock,
info.shadowCastingMode,
info.receiveShadows,
info.layerIdx,
drawCam,
info.lightProbeUsage,
null
);
idx++;
}
}
}
else
{
#if UNITY_EDITOR
draw_type = eDrawType.T3_DRAW_MESH;
#endif
var using_custom_culling = true;
if (using_custom_culling)
{
// culling
UpdateValidatedDrawCount(info, drawCam, more_actually_select);
lock (update_using_inst_idx_locker)
{
// normal draw mesh
for (int i = 0; i < info.validatedInstCount; i++)
{
var idx = info.usingIdx[i];
Graphics.DrawMesh(
mesh,
info.matrixWholeArray[idx],
material,
info.layerIdx,
drawCam, 0,
info.matPropBlock,
info.receiveShadows,
false
);
}
}
}
else
{
// 因为 Graphics.DrawMesh 内部有剔除 : https://blog.csdn.net/linjf520/article/details/113989289
// 所以我们可以不用自己剔除也是可以的
// 但是经过测试,如果每次 DrawMesh 都使用 unity 的 Culling
// 消耗会很大,还不如,在使用他自带的剔除前,先自己快速剔除一遍
for (int i = 0; i < info.matrixWholeArray.Length; i++)
{
Graphics.DrawMesh(
mesh,
info.matrixWholeArray[i],
material,
info.layerIdx,
drawCam, 0,
info.matPropBlock,
info.receiveShadows,
false
);
}
}
// normal draw mesh 有个问题,就是不会去绘制阴影
// 如果需要绘制阴影的话,那么性能就会更低
}
}
private int qt_culling_thread_sleep_interval_ms;
private Thread qt_culling_thread;
private bool is_culling_in_thread = false;
private bool stop_culling_in_thread = true;
public class CullingInThread_Arg
{
public InstancingSingleBatchingInfos info;
public MultiAABBSelectInfo<int> selectInfo;
}
public CullingInThread_Arg culling_arg = new CullingInThread_Arg();
private object culling_in_thread_locker = new object();
private object update_using_inst_idx_locker = new object();
private void UpdateValidatedDrawCount(InstancingSingleBatchingInfos info, Camera drawCam, bool more_actually_select)
{
lock (culling_in_thread_locker)
{
if (is_culling_in_thread)
{
return;
}
}
QTAABB.GetCameraAABBs(drawCam, cam_aabbs, frustum_AABB_level, frustum_h_padding);
if (qt_culling_thread == null)
{
if (Application.targetFrameRate == -1)
{
qt_culling_thread_sleep_interval_ms = 1;
}
else
{
qt_culling_thread_sleep_interval_ms = 1000 / Application.targetFrameRate;
}
qt_culling_thread = new Thread(culling_in_thread_method);
qt_culling_thread.IsBackground = true;
qt_culling_thread.Start();
}
var camWPos = drawCam.transform.position;
var orignalPos = new Vector2(camWPos.x, camWPos.z);
var selectInfo = new MultiAABBSelectInfo<int>
{
aabbs = cam_aabbs,
ret = info.selectIdxs,
cullingDistance = cullingDistance,
//cullingDistance = float.NaN,
orignalPos = orignalPos,
moreActually = more_actually_select,
};
lock (culling_in_thread_locker)
{
culling_arg.info = info;
culling_arg.selectInfo = selectInfo;
}
lock (update_using_inst_idx_locker)
{
var idx_count = 0;
foreach (var idx in info.usingIdx)
{
info.matrixArray4Culling[idx_count++] = info.matrixWholeArray[idx];
}
info.validatedInstCount = info.usingIdx.Count;
}
}
private void culling_in_thread_method()
{
while (stop_culling_in_thread)
{
// culling
lock (culling_in_thread_locker)
{
is_culling_in_thread = true;
culling_arg.info.quadtree.Select(culling_arg.selectInfo);
is_culling_in_thread = false;
}
// update inst idx
lock (update_using_inst_idx_locker)
{
culling_arg.info.usingIdx.Clear();
culling_arg.info.usingIdx.AddRange(culling_arg.info.selectIdxs);
}
//Thread.Sleep(qt_culling_thread_sleep_interval_ms);
Thread.Sleep(30);
}
}
private void OnDrawGizmos()
{
// 绘制 cam 的 aabb
if (draw_gizmos_cam_aabb) _DrawCameraAABB();
// 绘制 cam 的 wireframe
if (draw_gizmos_cam_wireframe) _DrawCameraWireframe();
// 绘制 qt 中每个枝干 的 aabb
if (draw_gizmos_qt_branches) _DrawQTBranchesAABBs();
// 绘制 qt 中每个 leaf 的 aabb
if (draw_gizmos_qt_leaves) _DrawQTLeavesAABBs();
// 绘制 qt 中每个在 frustum aabb 内的 leaf 的 aabb
if (draw_gizmos_qt_leaves_in_frustum_aabb) _DrawQTInFrustumLeavesAABBs();
// 绘制 qt culling distance 半径
if (draw_gizmos_qt_cullingDistancing) _DrawQTCullingDistance();
}
private void _DrawCameraAABB()
{
if (!cached_camera.TryGetValue(cfg.cam_path, out Camera cam))
{
cam = GameObject.Find(cfg.cam_path).GetComponent<Camera>();
cached_camera[cfg.cam_path] = cam;
}
foreach (var aabb in cam_aabbs)
{
_DrawQTAABB(aabb, cam_aabb_color);
}
}
private void _DrawCameraWireframe()
{
Gizmos.color = cam_frustum_color;
if (!cached_camera.TryGetValue(cfg.cam_path, out Camera cam))
{
cam = GameObject.Find(cfg.cam_path).GetComponent<Camera>();
cached_camera[cfg.cam_path] = cam;
}
// 可参考我以前的一篇文章:https://blog.csdn.net/linjf520/article/details/104994304#SceneGizmos_35
Matrix4x4 temp = Gizmos.matrix;
Gizmos.matrix = Matrix4x4.TRS(cam.transform.position, cam.transform.rotation, Vector3.one);
if (!cam.orthographic)
{
// 透视视锥
Gizmos.DrawFrustum(Vector3.zero, cam.fieldOfView, cam.farClipPlane, cam.nearClipPlane, cam.aspect);
}
else
{
// 正交 cube
var far = cam.farClipPlane;
var near = cam.nearClipPlane;
var delta_fn = far - near;
var half_height = cam.orthographicSize;
var half_with = cam.aspect * half_height;
var pos = Vector3.forward * (delta_fn * 0.5f + near);
var size = new Vector3(half_with * 2, half_height * 2, delta_fn);
Gizmos.DrawWireCube(pos, size);
}
Gizmos.matrix = temp;
}
private void _DrawQTBranchesAABBs()
{
foreach (var info in cfg.list)
{
if (info.quadtree == null)
{
continue;
}
_DrawBranch(info.quadtree.root, qt_branches_color);
}
}
private void _DrawQTLeavesAABBs()
{
foreach (var info in cfg.list)
{
if (info.quadtree == null)
{
continue;
}
_DrawLeafsOfBrances(info.quadtree.root, qt_leaves_color);
}
}
private void _DrawQTInFrustumLeavesAABBs()
{
foreach (var info in cfg.list)
{
lock (update_using_inst_idx_locker)
{
foreach (var idx in info.usingIdx)
{
var aabb = info.boundsList[idx];
_DrawQTAABB(aabb, qt_leaves_in_frustum_color);
}
}
}
}
private void _DrawQTCullingDistance()
{
Gizmos.color = qt_culling_distance_color;
const int SEGMENT = 36;
float add_angle = (Mathf.PI * 2) / SEGMENT;
if (!cached_camera.TryGetValue(cfg.cam_path, out Camera cam))
{
cam = GameObject.Find(cfg.cam_path).GetComponent<Camera>();
cached_camera[cfg.cam_path] = cam;
}
var cam_pos = cam.transform.position;
cam_pos.y = 0;
foreach (var info in cfg.list)
{
if (info.quadtree == null || float.IsNaN(cullingDistance))
{
continue;
}
float r = cullingDistance;
float firstX = float.NaN, firstY = float.NaN;
for (int i = 0; i < SEGMENT; i++)
{
var angle = i * add_angle;
var x = Mathf.Cos(angle) * r;
var y = Mathf.Sin(angle) * r;
if (float.IsNaN(firstX)) firstX = x;
if (float.IsNaN(firstY)) firstY = y;
if (i == SEGMENT - 1)
{
// final
Gizmos.DrawLine(cam_pos + new Vector3(x, 0, y), cam_pos + new Vector3(firstX, 0, firstY));
}
else
{
var next_angle = (i + 1) * add_angle;
var next_x = Mathf.Cos(next_angle) * r;
var next_y = Mathf.Sin(next_angle) * r;
Gizmos.DrawLine(cam_pos + new Vector3(x, 0, y), cam_pos + new Vector3(next_x, 0, next_y));
}
}
}
}
private void _DrawBranch(QuadTree<int>.Branch branch, Color color)
{
if (branch == null)
{
return;
}
// draw this branch
_DrawQTAABB(branch.aabb, color);
// draw sub branches
foreach (var b in branch.branches)
{
_DrawBranch(b, color);
}
}
private void _DrawLeafsOfBrances(QuadTree<int>.Branch branch, Color color)
{
if (branch == null)
{
return;
}
foreach (var b in branch.branches)
{
if (b == null)
{
continue;
}
foreach (var l in b.leaves)
{
_DrawQTAABB(l.aabb, color);
}
_DrawLeafsOfBrances(b, color);
}
}
private void _DrawBoundsXZ(Bounds bounds, Color color)
{
Gizmos.color = color;
var min = bounds.min;
var max = bounds.max;
var start_pos = min;
var end_pos = min;
end_pos.x = max.x;
Gizmos.DrawLine(start_pos, end_pos);
start_pos = end_pos;
end_pos = start_pos;
end_pos.z = max.z;
Gizmos.DrawLine(start_pos, end_pos);
start_pos = end_pos;
end_pos = start_pos;
end_pos.x = min.x;
Gizmos.DrawLine(start_pos, end_pos);
start_pos = end_pos;
end_pos = start_pos;
end_pos.z = min.z;
Gizmos.DrawLine(start_pos, end_pos);
}
private void _DrawQTAABB(QTAABB aabb, Color color)
{
Gizmos.color = color;
var min = aabb.min;
var max = aabb.max;
var start_pos = new Vector3(min.x, 0, min.y);
var end_pos = start_pos;
end_pos.x = max.x;
Gizmos.DrawLine(start_pos, end_pos);
start_pos = end_pos;
end_pos = start_pos;
end_pos.z = max.y;
Gizmos.DrawLine(start_pos, end_pos);
start_pos = end_pos;
end_pos = start_pos;
end_pos.x = min.x;
Gizmos.DrawLine(start_pos, end_pos);
start_pos = end_pos;
end_pos = start_pos;
end_pos.z = min.y;
Gizmos.DrawLine(start_pos, end_pos);
}
}
还有更简单的方式
使用 Unity 自带的类:CullingGroup 类,就不用自己写,但是这个类,能否放在多线程下正常运行,还没试过,所以最好还是自己写剔除
Project
GitHub 的先不放,后续回来完善后再放 GitHub
扩展
目前这些方式都会比较损耗 CPU 的方式
还有 GPU Driven 的方式:HiZ
会让 CPU 在渲染剔除这块会解放很多消耗