八叉樹分割3d 空間
【八叉樹:分割3D空間】
<1>目標:使用八叉樹分割3D空間,實現建立函式和插入函式
<2>思路:
<3>程式碼:
以下樹實現相關程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//封裝插入的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;
}
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 |
//節點定義(根據需求定義)
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>();
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//建立樹函式 中心 半寬 深度 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;
}
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
//插入函式 樹根節點 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 },
};
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//列印周圍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]);
}
}
}
|
以下樹建立測試程式碼
+ View Code
<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) 即位於第一層的第一象限
1 2 3 4 5 6 7 8 |
//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為正
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//建立子節點 每個節點的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);
}
}
|
插入函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
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 },
};
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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
原作者 :https://www.cnblogs.com/cocotang/p/10824958.html