线性八叉树
最近有个实际用例, 需要用到空间相关的测试, 大概如下:
一个水体的数据, 是由很多微小的点组成的, 这些点就是体积水, 我们要表现的话, 需要从这些点中找出能代表水体表面的点, 然后划分成三角面, 组成水体表面的Mesh, 之后加个材质就能展现了.
查找这些表面顶点的方法就比较麻烦了, 目前没有找到直接的方法, 貌似点云扫描数据的表面重建这些能有用, 没时间去看, 直接用八叉树划分空间, 然后把点数据放到相应的划分里去, 从上往下找到有点的空间, 基本就是表面了, 它的好处呢就是空间的划分可以自己设定大小, 等于是一种LOD的方法了, 这里实现了一个线性的八叉树, 可以对这种情况进行处理:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BoxOcTree<T> { public static System.Func<T, Vector3> DataToPosition; // cache datas public Bounds bounds; public BoxOcTree<T> parent; public BoxOcTree<T>[] trees = null; public List<T> datas = null; public bool ContainsData(T data) { if(DataToPosition != null) { var pos = DataToPosition.Invoke(data); return bounds.Contains(pos); } return false; } public bool ContainsPos(Vector3 pos, bool incursive = false) { var succ = bounds.Contains(pos); if(incursive) { if(trees != null && trees.Length > 0) { foreach(var tree in trees) { if(tree.ContainsPos(pos, incursive)) { return true; } } } } return succ; } public BoxOcTree<T> FindEndNode(Vector3 pos) { var succ = bounds.Contains(pos); if(false == succ) { return null; } if(trees != null && trees.Length > 0) { foreach(var tree in trees) { if(tree != null) { var tagNode = tree.FindEndNode(pos); if(tagNode != null) { return tagNode; } } } } return this; } public BoxOcTree<T> PushDataToTree(T data) { var pos = BoxOcTree<T>.DataToPosition(data); var tree = FindEndNode(pos); if(tree != null) { if(tree.datas == null) { tree.datas = new List<T>(); } tree.datas.Add(data); return tree; } return null; } public bool HasData(bool recursive) { var hasData = datas != null && datas.Count > 0; if(false == recursive) { return hasData; } var hasAnyData = hasData; if(false == hasAnyData) { if(trees != null) { for(int i = trees.Length - 1; i >= 0; i--) { var tree = trees[i]; if(tree != null && tree.HasData(recursive)) { hasAnyData = true; break; } } } } return hasAnyData; } public void ClearNoData(bool recursive) { if(parent == null) { return; } if(HasData(recursive) == false) { Destroy(recursive); } } public void Destroy(bool recursive) { if(parent != null) { var index = System.Array.IndexOf<BoxOcTree<T>>(parent.trees, this); if(index >= 0) { parent.trees[index] = null; if(recursive) { parent.ClearNoData(recursive); } } parent = null; } } public BoxOcTree<T> GetTailTree() { if(trees != null && trees.Length > 0) { foreach(var tree in trees) { if(tree != null) { return tree.GetTailTree(); } } } return this; } public bool IsSibling(BoxOcTree<T> other) { if(other != this && other.parent != null) { return other.parent == this.parent; } return false; } public BoxOcTree<T>[] GetSiblings() { if(this.parent != null) { return this.parent.trees; } return null; } }
目前的逻辑是先创建空间划分, 然后再把数据填入, 最后可以通过删除无数据的节点, 来加速计算.
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; public class BoxOcTreeUtilities { public const float Epsilon = 0.0001f; #region Main Funcs public static BoxOcTree<T> Create<T>(Bounds bounds, float minSize) { var root = new BoxOcTree<T>(); root.bounds = bounds; List<BoxOcTree<T>> boxOcTrees = new List<BoxOcTree<T>>(); OcTreeSpaceAccess(bounds, minSize, (_bounds) => { var child = Create<T>(_bounds, minSize); child.parent = root; boxOcTrees.Add(child); }); root.trees = boxOcTrees.ToArray(); return root; } public static BoxOcTree<T> FindEndNodeContainsPos<T>(BoxOcTree<T> root, Vector3 pos) { return root.FindEndNode(pos); } public static BoxOcTree<T> PushDataToTree<T>(BoxOcTree<T> root, T data, bool createNoExists = false) { var pos = BoxOcTree<T>.DataToPosition(data); var tree = FindEndNodeContainsPos(root, pos); if(tree != null) { if(tree.datas == null) { tree.datas = new List<T>(); } tree.datas.Add(data); } else { if(createNoExists) { } } return tree; } public static void LoopTreeEnd<T>(BoxOcTree<T> root, System.Action<BoxOcTree<T>> access) { try { if(root != null) { if(root.trees != null && root.trees.Length > 0) { foreach(var tree in root.trees) { LoopTreeEnd(tree, access); } } else { access.Invoke(root); } } } catch(System.Exception ex) { Debug.LogError(ex.ToString()); } } public static HashSet<BoxOcTree<T>> SelectSurface<T>(BoxOcTree<T> root) { var startPoints = new HashSet<BoxOcTree<T>>(); var center = root.bounds.center; var areaSize = root.bounds.size; var tailTree = root.GetTailTree(); var cellSize = tailTree.bounds.size; var xCount = Mathf.FloorToInt(areaSize.x / cellSize.x); var yCount = Mathf.FloorToInt(areaSize.y / cellSize.y); var zCount = Mathf.FloorToInt(areaSize.z / cellSize.z); var xStart = (-areaSize.x + cellSize.x) * 0.5f; var yStart = (areaSize.y - cellSize.y) * 0.5f; var zStart = (-areaSize.z + cellSize.z) * 0.5f; for(int i = 0; i < xCount; i++) { for(int j = 0; j < zCount; j++) { for(int k = 0; k < yCount; k++) { var x = xStart + i * cellSize.x; var y = yStart - k * cellSize.y; var z = zStart + j * cellSize.z; var point = new Vector3(x, y, z) + center; var endNode = root.FindEndNode(point); if(endNode != null && endNode.HasData(false)) { if(startPoints.Add(endNode) == false) { Debug.LogWarning("Exists"); } break; } } } } return startPoints; } public static Dictionary<int, HashSet<BoxOcTree<T>>> SeparateTree<T>(BoxOcTree<T> root) { Dictionary<int, HashSet<BoxOcTree<T>>> sep = new Dictionary<int, HashSet<BoxOcTree<T>>>(); HashSet<BoxOcTree<T>> all = new HashSet<BoxOcTree<T>>(); List<BoxOcTree<T>> cacheList = new List<BoxOcTree<T>>(); int index = 0; LoopTreeEnd(root, (_node) => { all.Add(_node); }); while(all.Count > 0) { var item = all.First(); var sibilings = item.GetSiblings(); var openList = new HashSet<BoxOcTree<T>>(); var closeList = sep.GetValue(index); foreach(var sibiling in sibilings) { if(sibiling != null) { openList.Add(sibiling); } } while(openList.Count > 0) { var first = openList.First(); if(first == null) { Debug.LogError("First NULL..."); } openList.Remove(first); closeList.Add(first); if(GetNearbyNodes(cacheList, first, root, true)) { foreach(var nearNode in cacheList) { if(nearNode != null) { if(closeList.Contains(nearNode) == false) { openList.Add(nearNode); } } } } } all.ExceptWith(closeList); index++; } return sep; } #endregion #region Help Funcs public static void OcTreeSpaceAccess(Bounds bounds, float minSize, System.Action<Bounds> boundsAccess) { var minSizeDouble = minSize * 2f; var boundMax = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z); if(boundMax >= minSizeDouble) { float[] xList = new float[0]; float[] yList = new float[0]; float[] zList = new float[0]; OcTreeSpaceSep(bounds.size.x, minSizeDouble, bounds.center.x, ref xList); OcTreeSpaceSep(bounds.size.y, minSizeDouble, bounds.center.y, ref yList); OcTreeSpaceSep(bounds.size.z, minSizeDouble, bounds.center.z, ref zList); foreach(var x in xList) { foreach(var y in yList) { foreach(var z in zList) { var center = new Vector3(x, y, z); var size_x = DistanceCheckTheSame(bounds.min.x, bounds.max.x, x) ? bounds.size.x : bounds.size.x * 0.5f; var size_y = DistanceCheckTheSame(bounds.min.y, bounds.max.y, y) ? bounds.size.y : bounds.size.y * 0.5f; var size_z = DistanceCheckTheSame(bounds.min.z, bounds.max.z, z) ? bounds.size.z : bounds.size.z * 0.5f; boundsAccess.Invoke(new Bounds(center, new Vector3(size_x, size_y, size_z))); } } } } } private static void OcTreeSpaceSep(float size, float minSize, float center, ref float[] array) { if(size >= minSize) { array = new float[2]; array[0] = center - (size * 0.25f); array[1] = center + (size * 0.25f); } else { array = new float[1]; array[0] = center; } } private static bool DistanceCheckTheSame(float left, float right, float value) { return Mathf.Abs(Mathf.Abs(left - value) - Mathf.Abs(right - value)) < Epsilon; } public static bool GetNearbyNodes<T>(List<BoxOcTree<T>> cacheList, BoxOcTree<T> node, BoxOcTree<T> root, bool checkTail = false) { cacheList.Clear(); var up = node.bounds.center + Vector3.Scale(Vector3.up, node.bounds.size); var down = node.bounds.center - Vector3.Scale(Vector3.up, node.bounds.size); var left = node.bounds.center + Vector3.Scale(Vector3.left, node.bounds.size); var right = node.bounds.center - Vector3.Scale(Vector3.left, node.bounds.size); var front = node.bounds.center + Vector3.Scale(Vector3.forward, node.bounds.size); var back = node.bounds.center - Vector3.Scale(Vector3.forward, node.bounds.size); Add(ref cacheList, root.FindEndNode(up), checkTail); Add(ref cacheList, root.FindEndNode(down), checkTail); Add(ref cacheList, root.FindEndNode(left), checkTail); Add(ref cacheList, root.FindEndNode(right), checkTail); Add(ref cacheList, root.FindEndNode(front), checkTail); Add(ref cacheList, root.FindEndNode(back), checkTail); return cacheList.Count > 0; } public static bool IsTailNode<T>(BoxOcTree<T> node) { return node != null && (node.trees == null || node.trees.Length == 0); } private static void Add<T>(ref List<BoxOcTree<T>> list, BoxOcTree<T> node, bool checkTail = false) { if(node != null) { if(checkTail && IsTailNode(node) == false) { return; } if(list == null) { list = new List<BoxOcTree<T>>(); } list.Add(node); } } #endregion #region Presetting #if UNITY_EDITOR [UnityEditor.InitializeOnLoadMethod()] #else [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] #endif public static void Init() { BoxOcTree<Vector3>.DataToPosition = (_raw) => { return _raw; }; BoxOcTree<Transform>.DataToPosition = (_trans) => { return _trans.position; }; BoxOcTree<GameObject>.DataToPosition = (_go) => { return _go.transform.position; }; Debug.Log("BoxOcTreeUtilities Init"); } #endregion }
第一版基本就是这样了.
-------------------------------------------------------------------
第二版, 在创建八叉树根节点的时候, 不创建任何空间划分, 在需要获取最后子节点的时候再进行查找和创建, 可以节省大量的时间. 因为即使提前划分好, 在最终阶段还是要清理没有数据的多余划分的...
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BoxOcTree<T> { public static System.Func<T, Vector3> DataToPosition; // cache datas public Bounds bounds; public BoxOcTree<T> parent; public BoxOcTree<T>[] trees = null; public List<T> datas = null; public int layer = 1; public bool ContainsData(T data) { if(DataToPosition != null) { var pos = DataToPosition.Invoke(data); return bounds.Contains(pos); } return false; } public bool ContainsPos(Vector3 pos) { var succ = bounds.Contains(pos); return succ; } public BoxOcTree<T> FindEndNode(Vector3 pos) { var succ = bounds.Contains(pos); if(false == succ) { return null; } if(trees != null && trees.Length > 0) { foreach(var tree in trees) { if(tree != null) { var tagNode = tree.FindEndNode(pos); if(tagNode != null) { return tagNode; } } } } return this; } public void AddData(T data) { if(this.datas == null) { this.datas = new List<T>(); } this.datas.Add(data); } public BoxOcTree<T> PushDataToTree(T data) { var pos = BoxOcTree<T>.DataToPosition(data); var tree = FindEndNode(pos); if(tree != null) { tree.AddData(data); return tree; } return null; } public bool HasData(bool recursive) { var hasData = datas != null && datas.Count > 0; if(false == recursive) { return hasData; } var hasAnyData = hasData; if(false == hasAnyData) { if(trees != null) { for(int i = trees.Length - 1; i >= 0; i--) { var tree = trees[i]; if(tree != null && tree.HasData(recursive)) { hasAnyData = true; break; } } } } return hasAnyData; } public void ClearNoData(bool recursive) { if(parent == null) { return; } if(HasData(recursive) == false) { Destroy(recursive); } } public void Destroy(bool recursive) { if(parent != null) { var index = System.Array.IndexOf<BoxOcTree<T>>(parent.trees, this); if(index >= 0) { parent.trees[index] = null; if(recursive) { parent.ClearNoData(recursive); } } parent = null; } } public BoxOcTree<T> GetTailTree() { if(trees != null && trees.Length > 0) { foreach(var tree in trees) { if(tree != null) { return tree.GetTailTree(); } } } return this; } public bool IsSibling(BoxOcTree<T> other) { if(other != this && other.parent != null) { return other.parent == this.parent; } return false; } public BoxOcTree<T>[] GetSiblings() { if(this.parent != null) { return this.parent.trees; } return null; } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; public class BoxOcTreeUtilities { public const float Epsilon = 0.0001f; public const int TreeBufferSize = 8; #region Main Funcs public static BoxOcTree<T> Create<T>(Bounds bounds, float minSize, int currentLayer = 1) { var root = CreateEmpty<T>(bounds, null, currentLayer); var nextLayer = currentLayer + 1; List<BoxOcTree<T>> boxOcTrees = new List<BoxOcTree<T>>(); OcTreeSpaceAccess(bounds, minSize, (_bounds) => { var child = Create<T>(_bounds, minSize, nextLayer); child.parent = root; boxOcTrees.Add(child); return true; }); root.trees = boxOcTrees.ToArray(); return root; } public static BoxOcTree<T> CreateEmpty<T>(Bounds bounds, BoxOcTree<T> parent = null, int currentLayer = 1) { var root = new BoxOcTree<T>(); root.bounds = bounds; root.layer = currentLayer; root.parent = parent; return root; } public static BoxOcTree<T> RequireTargetNode<T>(BoxOcTree<T> treeNode, float minSize, Vector3 targetPos) { BoxOcTree<T> targetNode = treeNode.FindEndNode(targetPos); if(targetNode == null) { return null; } if(targetNode != null && IsTailNode(targetNode, minSize)) { return targetNode; } var nextLayer = targetNode.layer + 1; OcTreeSpaceAccess(targetNode.bounds, minSize, (_bounds) => { if(_bounds.Contains(targetPos)) { var child = CreateEmpty<T>(_bounds, targetNode, nextLayer); if(targetNode.trees == null) { targetNode.trees = GetTreeBuffer<T>(); } else if(targetNode.trees.Length != TreeBufferSize) { var temp = targetNode.trees; targetNode.trees = GetTreeBuffer<T>(); System.Array.Copy(temp, 0, targetNode.trees, 0, temp.Length); } for(int i = 0; i < targetNode.trees.Length; i++) { if(targetNode.trees[i] == null) { targetNode.trees[i] = child; break; } } targetNode = RequireTargetNode(child, minSize, targetPos); return false; } return true; }); return targetNode; } public static BoxOcTree<T> PushDataToTree<T>(BoxOcTree<T> root, T data, float minSize = 1f, bool createNoExists = true) { var pos = BoxOcTree<T>.DataToPosition(data); var tree = createNoExists ? RequireTargetNode(root, minSize, pos) : root.FindEndNode(pos); if(tree != null) { tree.AddData(data); } return tree; } public static void LoopTreeEnd<T>(BoxOcTree<T> root, System.Action<BoxOcTree<T>> access) { try { if(root != null) { if(root.trees != null && root.trees.Length > 0) { foreach(var tree in root.trees) { LoopTreeEnd(tree, access); } } else { access.Invoke(root); } } } catch(System.Exception ex) { Debug.LogError(ex.ToString()); } } public static HashSet<BoxOcTree<T>> SelectSurface<T>(BoxOcTree<T> root) { var startPoints = new HashSet<BoxOcTree<T>>(); var center = root.bounds.center; var areaSize = root.bounds.size; var tailTree = root.GetTailTree(); var cellSize = tailTree.bounds.size; var xCount = Mathf.FloorToInt(areaSize.x / cellSize.x); var yCount = Mathf.FloorToInt(areaSize.y / cellSize.y); var zCount = Mathf.FloorToInt(areaSize.z / cellSize.z); var xStart = (-areaSize.x + cellSize.x) * 0.5f; var yStart = (areaSize.y - cellSize.y) * 0.5f; var zStart = (-areaSize.z + cellSize.z) * 0.5f; for(int i = 0; i < xCount; i++) { for(int j = 0; j < zCount; j++) { for(int k = 0; k < yCount; k++) { var x = xStart + i * cellSize.x; var y = yStart - k * cellSize.y; var z = zStart + j * cellSize.z; var point = new Vector3(x, y, z) + center; var endNode = root.FindEndNode(point); if(endNode != null && endNode.HasData(false)) { if(startPoints.Add(endNode) == false) { Debug.LogWarning("Exists"); } break; } } } } return startPoints; } public static Dictionary<int, HashSet<BoxOcTree<T>>> SeparateTree<T>(BoxOcTree<T> root) { Dictionary<int, HashSet<BoxOcTree<T>>> sep = new Dictionary<int, HashSet<BoxOcTree<T>>>(); HashSet<BoxOcTree<T>> all = new HashSet<BoxOcTree<T>>(); List<BoxOcTree<T>> cacheList = new List<BoxOcTree<T>>(); int index = 0; LoopTreeEnd(root, (_node) => { all.Add(_node); }); while(all.Count > 0) { var item = all.First(); var sibilings = item.GetSiblings(); var openList = new HashSet<BoxOcTree<T>>(); var closeList = sep.GetValue(index); foreach(var sibiling in sibilings) { if(sibiling != null) { openList.Add(sibiling); } } while(openList.Count > 0) { var first = openList.First(); if(first == null) { Debug.LogError("First NULL..."); } openList.Remove(first); closeList.Add(first); if(GetNearbyNodes(cacheList, first, root, true)) { foreach(var nearNode in cacheList) { if(nearNode != null) { if(closeList.Contains(nearNode) == false) { openList.Add(nearNode); } } } } } all.ExceptWith(closeList); index++; } return sep; } #endregion #region Help Funcs public static void OcTreeSpaceAccess(Bounds bounds, float minSize, System.Func<Bounds, bool> boundsAccess) { var minSizeDouble = minSize * 2f; var boundMax = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z); if(boundMax >= minSizeDouble) { float[] xList = new float[0]; float[] yList = new float[0]; float[] zList = new float[0]; OcTreeSpaceSep(bounds.size.x, minSizeDouble, bounds.center.x, ref xList); OcTreeSpaceSep(bounds.size.y, minSizeDouble, bounds.center.y, ref yList); OcTreeSpaceSep(bounds.size.z, minSizeDouble, bounds.center.z, ref zList); bool running = true; foreach(var x in xList) { foreach(var y in yList) { foreach(var z in zList) { var center = new Vector3(x, y, z); var size_x = DistanceCheckTheSame(bounds.min.x, bounds.max.x, x) ? bounds.size.x : bounds.size.x * 0.5f; var size_y = DistanceCheckTheSame(bounds.min.y, bounds.max.y, y) ? bounds.size.y : bounds.size.y * 0.5f; var size_z = DistanceCheckTheSame(bounds.min.z, bounds.max.z, z) ? bounds.size.z : bounds.size.z * 0.5f; running = boundsAccess.Invoke(new Bounds(center, new Vector3(size_x, size_y, size_z))); if(false == running) { break; } } if(false == running) { break; } } if(false == running) { break; } } } } private static void OcTreeSpaceSep(float size, float minSize, float center, ref float[] array) { if(size >= minSize) { array = new float[2]; array[0] = center - (size * 0.25f); array[1] = center + (size * 0.25f); } else { array = new float[1]; array[0] = center; } } private static bool DistanceCheckTheSame(float left, float right, float value) { return Mathf.Abs(Mathf.Abs(left - value) - Mathf.Abs(right - value)) < Epsilon; } public static bool GetNearbyNodes<T>(List<BoxOcTree<T>> cacheList, BoxOcTree<T> node, BoxOcTree<T> root, bool checkTail = false) { cacheList.Clear(); var up = node.bounds.center + Vector3.Scale(Vector3.up, node.bounds.size); var down = node.bounds.center - Vector3.Scale(Vector3.up, node.bounds.size); var left = node.bounds.center + Vector3.Scale(Vector3.left, node.bounds.size); var right = node.bounds.center - Vector3.Scale(Vector3.left, node.bounds.size); var front = node.bounds.center + Vector3.Scale(Vector3.forward, node.bounds.size); var back = node.bounds.center - Vector3.Scale(Vector3.forward, node.bounds.size); Add(ref cacheList, root.FindEndNode(up), checkTail); Add(ref cacheList, root.FindEndNode(down), checkTail); Add(ref cacheList, root.FindEndNode(left), checkTail); Add(ref cacheList, root.FindEndNode(right), checkTail); Add(ref cacheList, root.FindEndNode(front), checkTail); Add(ref cacheList, root.FindEndNode(back), checkTail); return cacheList.Count > 0; } public static bool NodeWithoutChild<T>(BoxOcTree<T> node) { return node != null && (node.trees == null || node.trees.Length == 0); } public static bool IsTailNode<T>(BoxOcTree<T> node, float minSize) { if(NodeWithoutChild(node)) { var extents = node.bounds.extents; if(extents.x <= minSize && extents.y <= minSize && extents.z <= minSize) { return true; } } return false; } private static void Add<T>(ref List<BoxOcTree<T>> list, BoxOcTree<T> node, bool checkTail = false) { if(node != null) { if(checkTail && NodeWithoutChild(node) == false) { return; } if(list == null) { list = new List<BoxOcTree<T>>(); } list.Add(node); } } public static BoxOcTree<T>[] GetTreeBuffer<T>() { return new BoxOcTree<T>[TreeBufferSize]; } #endregion #region Presetting #if UNITY_EDITOR [UnityEditor.InitializeOnLoadMethod()] #else [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] #endif public static void Init() { BoxOcTree<Vector3>.DataToPosition = (_raw) => { return _raw; }; BoxOcTree<Transform>.DataToPosition = (_trans) => { return _trans.position; }; BoxOcTree<GameObject>.DataToPosition = (_go) => { return _go.transform.position; }; Debug.Log("BoxOcTreeUtilities Init"); } #endregion }