Unity - 撸一个简单版本的 四叉树 + 视锥cascaded + 多线程按分类剔除 + GPU instancing,用于场景剔除 (还有BUG,后续再优化)

本文介绍如何在Unity中实现GPU Instancing,包括提取场景中可进行实例化的GameObject信息、编写自定义工具进行实例化数据的导出及优化,同时探讨了如何结合四叉树进行高效的剔除操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


环境

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 在渲染剔除这块会解放很多消耗

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值