【八叉樹:分割3D空間】


<1>目標:使用八叉樹分割3D空間,實現創建函數和插入函數

<2>思路:

<3>代碼:

以下樹實現相關代碼

    //封裝插入的obj結構(根據項目自己定義)
public class OCObj {
//Unity Obj引用 public GameObject gameObj;//
//物體半徑(寬度) public float halfWidth; //center在obj身上取 //public Vector3 center = Vector3.zero; //引用到的樹節點lst 用於移除 public Node linkNode;
//位置 public Vector3 center { get { return gameObj.transform.position; } } }

  

    //節點定義(根據需求定義)
    public class Node
    {
        //節點中心
        public Vector3 center = Vector3.zero;
        //節點寬度
        public float halfWidth = 0;
        //緩存插入的Obj
        public List<OCObj> objLst = null;//需要再new
        //子節點 需要再new
        public List<Node> childs = new List<Node>();
    }

  

        //創建樹函數 中心 半寬 深度
public static Node BuildOCTree(Vector3 center, float halfWidth, int stopDepth) { if (stopDepth < 0) return null; else { Node node = new Node(); node.center = center; node.halfWidth = halfWidth; //創建子節點 每個節點的3個軸偏移 即為+- halfWidth*0.5f Vector3 offset = Vector3.zero; float step = halfWidth * 0.5f; for (int i = 0; i < 8; i++) { //8個點 //上下排序 offset.y = i % 2 == 0 ? step : -step; offset.z = i <= 3 ? step : -step; offset.x = i <= 1 || i >= 6 ? step : -step; Node child = BuildOCTree(center + offset, step, stopDepth - 1); if (child != null) { node.childs.Add(child); } } return node; } }

  

        //插入函數 樹根節點 Obj
public static void Insert(Node root, OCObj obj) { int index = 0; int x = 0; int y = 0; int z = 0; bool isPress = false;//是否占用了節點的2個格子 Dictionary<int, int> map = new Dictionary<int, int>(); for (int i = 0; i < 3; i++) { float delta = obj.center[i] - root.center[i]; if (Mathf.Abs(delta) < obj.halfWidth) { isPress = true; } if (i == 0) { x = delta > 0 ? 1 : -1; } else if (i == 1) { y = delta > 0 ? 1 : -1; } else { z = delta > 0 ? 1 : -1; } } index = indexMap[z * 100 + y * 10 + x]; //壓線了 或者 沒有再深的層次了 則加入到當前節點 if (isPress || root.childs.Count <= 0) { if (root.objLst == null) { root.objLst = new List<OCObj>(); } root.objLst.Add(obj); obj.linkNode = root; } else { Insert(root.childs[index], obj); } } static Dictionary<int, int> indexMap = new Dictionary<int, int>() { { 100+10+1,0 },//1 1 1 = 0 { 100-10+1,1 },//1 0 1 = 1 { 100+10-1,2 },//1 1 0 = 2 { 100-10-1, 3 },//1 0 0 = 3 { -100+10-1, 4 },//0 1 0 = 4 101 = 4 { -100-10 -1, 5 }, { -100+10 +1, 6 }, { -100-10 +1, 7 }, };

  

        //打印周圍obj.name
        public static void PrintCloserObjs(Node node) {
            if (node.objLst != null && node.objLst.Count > 0) {
                for (int i = 0; i < node.objLst.Count; i++)
                {
                    Debug.Log("name:  " + node.objLst[i].gameObj.name);
                }
            }
            if (node.childs != null && node.childs.Count > 0) {
                for (int i = 0; i < node.childs.Count; i++)
                {
                    PrintCloserObjs(node.childs[i]);
                }
            }
        }

  以下樹創建測試代碼

using UnityEngine;
using System.Collections.Generic;
using OCTree;
using System;

public class OCTreeMain : MonoBehaviour
{

    public float halfWidth = 1000;
    public int depth = 5;
    public Vector3 center = Vector3.zero;
    public GameObject gameObj;

    Node root = null;
    List<OCObj> ocLst = new List<OCObj>();
    int uid = 0;
    
    void Start()
    {
        root = OCTree.OCUtils.BuildOCTree(center, halfWidth, depth);        
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            uid++;
            OCObj obj = new OCObj();
            obj.gameObj = new GameObject("empty"+ uid); //gameObj;
            obj.gameObj.transform.position = gameObj.transform.position;
            obj.halfWidth = gameObj.transform.localScale.x / 2;
            OCUtils.Insert(root, obj);
            ocLst.Add(obj);
            System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
            stopwatch.Start();
            for (int i = 0; i < 10000; i++)
            {
                OCUtils.Remove(obj);
                OCUtils.Insert(root, obj);
            }
            stopwatch.Stop();
            Debug.Log("插入耗時:  "+stopwatch.ElapsedMilliseconds);
        }
        if (Input.GetKeyDown(KeyCode.P)) {
            int max = ocLst.Count;
            int idx = UnityEngine.Random.Range(0, max);
            if (idx > 0) {
                OCObj obj = ocLst[idx];
                Debug.LogError("打印:  " + obj.gameObj.name + "  周圍的物體");
                OCUtils.PrintCloserObjs(obj.linkNode);
            }
        }
    }

    int gizFrameNum = 0;
    int gizShowNum = 0;
    void OnDrawGizmos()
    {
        gizShowNum++;
        if (gizShowNum >= 100)
        {
            gizShowNum = 0;
            if (gizFrameNum >= 8) gizFrameNum = 0;
            gizFrameNum++;
        }

        //展示所有
        if (root != null)
        {
            drawOCTree(root);
        }
        //一層一層展示
        //8個葉子分別展示
        //int idx = gizFrameNum % 8;
        //if (root != null) {
        //    Node node = root.childs[idx];
        //    if (node != null) {
        //        drawOCTree(node);
        //    }            
        //}
    }

    void drawOCTree(Node node)
    {
        //繪制里面的物體
        if (node.objLst != null && node.objLst.Count > 0)
        {
            for (int i = 0; i < node.objLst.Count; i++)
            {
                OCObj obj = node.objLst[i];
                Gizmos.DrawCube(obj.center, new Vector3(obj.halfWidth * 2, obj.halfWidth * 2, obj.halfWidth * 2));
            }
        }
        //繪制線框
        List<Node> childs = node.childs;
        if (childs != null && childs.Count > 0)
        {
            for (int i = 0; i < childs.Count; i++)
            {
                Node child = childs[i];
                drawOCTree(child);
                if (child.childs.Count <= 0)
                {
                    //繪制8個
                    Gizmos.color = getColor(i);
                    //8個頂點             
                    float halfWidth = child.halfWidth;
                    Vector3 p0 = child.center + new Vector3(halfWidth, halfWidth, halfWidth);
                    Vector3 p2 = child.center + new Vector3(-halfWidth, halfWidth, halfWidth);
                    Vector3 p4 = child.center + new Vector3(-halfWidth, halfWidth, -halfWidth);
                    Vector3 p6 = child.center + new Vector3(halfWidth, halfWidth, -halfWidth);

                    Vector3 p1 = child.center + new Vector3(halfWidth, -halfWidth, halfWidth);
                    Vector3 p3 = child.center + new Vector3(-halfWidth, -halfWidth, halfWidth);
                    Vector3 p5 = child.center + new Vector3(-halfWidth, -halfWidth, -halfWidth);
                    Vector3 p7 = child.center + new Vector3(halfWidth, -halfWidth, -halfWidth);
                    Gizmos.DrawLine(p0, p2);
                    Gizmos.DrawLine(p2, p4);
                    Gizmos.DrawLine(p4, p6);
                    Gizmos.DrawLine(p6, p0);

                    Gizmos.DrawLine(p1, p3);
                    Gizmos.DrawLine(p3, p5);
                    Gizmos.DrawLine(p5, p7);
                    Gizmos.DrawLine(p7, p1);

                    Gizmos.DrawLine(p0, p1);
                    Gizmos.DrawLine(p2, p3);
                    Gizmos.DrawLine(p4, p5);
                    Gizmos.DrawLine(p6, p7);
                }
            }
        }
    }

    Color getColor(int index)
    {
        Color color = Color.white;
        int idx = index % 4;
        switch (idx)
        {
            case 0:
                color = Color.blue;
                break;
            case 1:
                color = Color.yellow;
                break;
            case 2:
                color = Color.green;
                break;
            case 3:
                color = Color.red;
                break;
            case 4:
                color = Color.red;
                break;
        }
        return color;
    }


}

  

<4>核心

創建函數中,2種創建方式,最終我們選取第二種方式創建子節點,因為我們需要更高效的插入

1.紅色代碼計算8個節點中心 容易理解:8個節點在3D空間中分為2層,從上層第一象限Index=0到第二層第一象限Index=1到第一層第二象限到第二層第二象限到...

2.藍色代碼用一種比較特殊的方式計算出8個節點中心

   8個節點的Index區間[0-7],而0-7可以用二進制表示為8個象限軸方向

   1,2,4的二進制可以表示三條坐標軸

   在遍歷0-7的時候,每個數字的二進制與1,2,4求 & 運算

   可以求出每個節點的軸方向:我們下面以1計算X軸 2計算Y軸 4計算Z軸  1=0001  2=0010  4=0100

   例如節點index=0與 1,2,4 做&運算 0=0000   求出的三種&運算都為0000 所以index=0的節點坐標=父節點坐標 + Vector3(-step,-step,-step) 即位於第二層的第三象限

   例如節點index=1與 1,2,4 做&運算 0=0001   X軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(step,-step,-step) 即位於第二層的第四象限

   例如節點index=2與 1,2,4 做&運算 0=0010   Y軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(-step,step,-step) 即位於第一層的第三象限

   例如節點index=3與 1,2,4 做&運算 0=0011   XY軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(step,step,-step) 即位於第一層的第四象限

   例如節點index=4與 1,2,4 做&運算 0=0100   Z軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(-step,-step,step) 即位於第二層的第二象限

   例如節點index=5與 1,2,4 做&運算 0=0101   XZ軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(step,-step,step) 即位於第二層的第一象限

   例如節點index=6與 1,2,4 做&運算 0=0110   YZ軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(-step,step,step) 即位於第一層的第二象限

   例如節點index=7與 1,2,4 做&運算 0=0111   XYZ軸為正 所以index=1的節點坐標=父節點坐標 + Vector3(step,step,step) 即位於第一層的第一象限

  

            //0  0/2 = 00            0000 //XYZ軸為負
 //1 1/2 = 01 0001 //Z軸為正 也可以當做X軸
 //2 10 01 0010 //Y軸為正
            //3   11   01            0011 //YZ為正
 //4 20 10 01 0100 //X軸為正 也可以當做Z軸
            //5   21   10  01        0101 //XZ為正
            //6   30   11  01        0110 //XY為正
            //7   31   11  01        0111 //XYZ為正

  

                //創建子節點  每個節點的3個軸偏移 即為+- halfWidth*0.5f
                Vector3 offset = Vector3.zero;
                float step = halfWidth * 0.5f;
                for (int i = 0; i < 8; i++)
                {
                    //8個點
                    //上下排序 
                    offset.y = i % 2 == 0 ? step : -step;
                    offset.z = i <= 3 ? step : -step;
                    offset.x = i <= 1 || i >= 6 ? step : -step; //offset.x = (i & 1) > 0 ? step : -step; //offset.y = (i & 2) > 0 ? step : -step; //offset.z = (i & 4) > 0 ? step : -step;
                    Node child = BuildOCTree(center + offset, step, stopDepth - 1);
                    if (child != null)
                    {
                        node.childs.Add(child);
                    }
                }

  

插入函數

        public static void Insert(Node root, OCObj obj)
        {
            int index = 0;
            int x = 0;
            int y = 0;
            int z = 0;
            bool isPress = false;//是否占用了節點的2個格子
            Dictionary<int, int> map = new Dictionary<int, int>();
            for (int i = 0; i < 3; i++)
            {
                float delta = obj.center[i] - root.center[i];
                if (Mathf.Abs(delta) < obj.halfWidth)
                {
                    isPress = true;
                }
                if (i == 0)
                {
                    x = delta > 0 ? 1 : -1;
                }
                else if (i == 1)
                {
                    y = delta > 0 ? 1 : -1;
                }
                else
                {
                    z = delta > 0 ? 1 : -1;
                }
            }
            index = indexMap[z * 100 + y * 10 + x];
            //壓線了 或者 沒有再深的層次了 則加入到當前節點
            if (isPress || root.childs.Count <= 0)
            {
                if (root.objLst == null)
                {
                    root.objLst = new List<OCObj>();
                }
                root.objLst.Add(obj);
                obj.linkNode = root;
            }
            else
            {
                Insert(root.childs[index], obj);
            }
        }

        static Dictionary<int, int> indexMap = new Dictionary<int, int>() {
            { 100+10+1,0 },//1  1  1 = 0
            { 100-10+1,1 },//1  0   1 = 1
            { 100+10-1,2 },//1   1  0  = 2
            { 100-10-1, 3 },//1   0  0 = 3
            { -100+10-1, 4 },//0  1  0 = 4   101 = 4 
            { -100-10 -1, 5 },
            { -100+10 +1, 6 },
            { -100-10 +1, 7 },
        };

  

        public static void Insert(Node root, OCObj obj)
        {
            int index = 0;
            bool isPress = false;//是否占用了節點的2個格子
 for (int i = 0; i < 3; i++) { float delta = obj.center[i] - root.center[i]; if (Mathf.Abs(delta) < obj.halfWidth) { isPress = true; break; } if (delta > 0.0f) index |= (1 << i); }
            //壓線了 或者 沒有再深的層次了 則加入到當前節點
            if (isPress || root.childs.Count <= 0)
            {
                if (root.objLst == null)
                {
                    root.objLst = new List<OCObj>();
                }
                root.objLst.Add(obj);
                obj.linkNode = root;
            }
            else
            {
                Insert(root.childs[index], obj);
            }
        }

  

上面的2種插入函數對應了2種創建函數

第一種插入函數比較容易理解:根據插入obj的坐標與節點坐標進行對比,確定XYZ的正負,組合成key = Z*100+Y*10+X,然后再字典中取對應的index

第一種10000次測試,深度為4,平均開銷為25ms(new字典 3次遍歷 if else3次判斷)

第二種10000次測試,深度為4,平均開銷為5ms(至多3次遍歷,如果壓線就break插入當前節點,兩種方式中讀取了gameObject的transform這個可以統一優化todo)

第二種函數函數最難理解的是確定obj的index

思路:

遍歷3次 i區間[0-2] 

默認index=0 使用二進制表示為 0000三個軸都為負

每次都進行了 1<<i 運算 可以發現三次運算結果是 1<<0 = 0001  1<<1 = 1*2^1 = 2 = 0010 1<<2 =  1*2^2 = 4 = 0100 即為XYZ三個軸 

當i=0切obj.x-node.x > 0,index = 0000 | (1<<0) = 0000 | (1*2^0) = 0000 | 1 = 0000 | 0001 = 0001 =  1 

第一次可以確定X 第二次可以確定Y 第三次可以確定Z 三次的index都是在與之前的index做 | 運算,可以求出並集,最終確定index

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM