using System.Collections.Generic; using UnityEngine; // A node in a BoundsOctree // Copyright 2014 Nition, BSD licence (see LICENCE file). http://nition.co namespace AwesomeTechnologies.External.Octree { public class BoundsOctreeNode { // Centre of this node public Vector3 Center { get; private set; } // Length of this node if it has a looseness of 1.0 public float BaseLength { get; private set; } // Looseness value for this node float looseness; // Minimum size for a node in this octree float minSize; // Actual length of sides, taking the looseness value into account float adjLength; // Bounding box that represents this node Bounds bounds = default(Bounds); // Objects in this node readonly List objects = new List(); // Child nodes, if any BoundsOctreeNode[] children = null; // Bounds of potential children to this node. These are actual size (with looseness taken into account), not base size Bounds[] childBounds; // If there are already numObjectsAllowed in a node, we split it into children // A generally good number seems to be something around 8-15 const int numObjectsAllowed = 8; // An object in the octree class OctreeObject { public T Obj; public Bounds Bounds; } /// /// Constructor. /// /// Length of this node, not taking looseness into account. /// Minimum size of nodes in this octree. /// Multiplier for baseLengthVal to get the actual size. /// Centre position of this node. public BoundsOctreeNode(float baseLengthVal, float minSizeVal, float loosenessVal, Vector3 centerVal) { SetValues(baseLengthVal, minSizeVal, loosenessVal, centerVal); } // #### PUBLIC METHODS #### /// /// Add an object. /// /// Object to add. /// 3D bounding box around the object. /// True if the object fits entirely within this node. public bool Add(T obj, Bounds objBounds) { if (!Encapsulates(bounds, objBounds)) { return false; } SubAdd(obj, objBounds); return true; } /// /// Remove an object. Makes the assumption that the object only exists once in the tree. /// /// Object to remove. /// True if the object was removed successfully. public bool Remove(T obj) { bool removed = false; for (int i = 0; i < objects.Count; i++) { if (objects[i].Obj.Equals(obj)) { removed = objects.Remove(objects[i]); break; } } if (!removed && children != null) { for (int i = 0; i < 8; i++) { removed = children[i].Remove(obj); if (removed) break; } } if (removed && children != null) { // Check if we should merge nodes now that we've removed an item if (ShouldMerge()) { Merge(); } } return removed; } /// /// Removes the specified object at the given position. Makes the assumption that the object only exists once in the tree. /// /// Object to remove. /// 3D bounding box around the object. /// True if the object was removed successfully. public bool Remove(T obj, Bounds objBounds) { if (!Encapsulates(bounds, objBounds)) { return false; } return SubRemove(obj, objBounds); } /// /// Check if the specified bounds intersect with anything in the tree. See also: GetColliding. /// /// Bounds to check. /// True if there was a collision. public bool IsColliding(ref Bounds checkBounds) { // Are the input bounds at least partially in this node? if (!bounds.Intersects(checkBounds)) { return false; } // Check against any objects in this node for (int i = 0; i < objects.Count; i++) { if (objects[i].Bounds.Intersects(checkBounds)) { return true; } } // Check children if (children != null) { for (int i = 0; i < 8; i++) { if (children[i].IsColliding(ref checkBounds)) { return true; } } } return false; } /// /// Check if the specified ray intersects with anything in the tree. See also: GetColliding. /// /// Ray to check. /// Distance to check. /// True if there was a collision. public bool IsColliding(ref Ray checkRay, float maxDistance = float.PositiveInfinity) { // Is the input ray at least partially in this node? float distance; if (!bounds.IntersectRay(checkRay, out distance) || distance > maxDistance) { return false; } // Check against any objects in this node for (int i = 0; i < objects.Count; i++) { if (objects[i].Bounds.IntersectRay(checkRay, out distance) && distance <= maxDistance) { return true; } } // Check children if (children != null) { for (int i = 0; i < 8; i++) { if (children[i].IsColliding(ref checkRay, maxDistance)) { return true; } } } return false; } /// /// Returns an array of objects that intersect with the specified bounds, if any. Otherwise returns an empty array. See also: IsColliding. /// /// Bounds to check. Passing by ref as it improves performance with structs. /// List result. /// Objects that intersect with the specified bounds. public void GetColliding(ref Bounds checkBounds, List result) { // Are the input bounds at least partially in this node? if (!bounds.Intersects(checkBounds)) { return; } // Check against any objects in this node for (int i = 0; i < objects.Count; i++) { if (objects[i].Bounds.Intersects(checkBounds)) { result.Add(objects[i].Obj); } } // Check children if (children != null) { for (int i = 0; i < 8; i++) { children[i].GetColliding(ref checkBounds, result); } } } /// /// Returns an array of objects that intersect with the specified ray, if any. Otherwise returns an empty array. See also: IsColliding. /// /// Ray to check. Passing by ref as it improves performance with structs. /// Distance to check. /// List result. /// Objects that intersect with the specified ray. public void GetColliding(ref Ray checkRay, List result, float maxDistance = float.PositiveInfinity) { float distance; // Is the input ray at least partially in this node? if (!bounds.IntersectRay(checkRay, out distance) || distance > maxDistance) { return; } // Check against any objects in this node for (int i = 0; i < objects.Count; i++) { if (objects[i].Bounds.IntersectRay(checkRay, out distance) && distance <= maxDistance) { result.Add(objects[i].Obj); } } // Check children if (children != null) { for (int i = 0; i < 8; i++) { children[i].GetColliding(ref checkRay, result, maxDistance); } } } /// /// Set the 8 children of this octree. /// /// The 8 new child nodes. public void SetChildren(BoundsOctreeNode[] childOctrees) { if (childOctrees.Length != 8) { Debug.LogError("Child octree array must be length 8. Was length: " + childOctrees.Length); return; } children = childOctrees; } public Bounds GetBounds() { return bounds; } /// /// Draws node boundaries visually for debugging. /// Must be called from OnDrawGizmos externally. See also: DrawAllObjects. /// /// Used for recurcive calls to this method. public void DrawAllBounds(float depth = 0) { float tintVal = depth / 7; // Will eventually get values > 1. Color rounds to 1 automatically Gizmos.color = new Color(tintVal, 0, 1.0f - tintVal); Bounds thisBounds = new Bounds(Center, new Vector3(adjLength, adjLength, adjLength)); Gizmos.DrawWireCube(thisBounds.center, thisBounds.size); if (children != null) { depth++; for (int i = 0; i < 8; i++) { children[i].DrawAllBounds(depth); } } Gizmos.color = Color.white; } /// /// Draws the bounds of all objects in the tree visually for debugging. /// Must be called from OnDrawGizmos externally. See also: DrawAllBounds. /// public void DrawAllObjects() { float tintVal = BaseLength / 20; Gizmos.color = new Color(0, 1.0f - tintVal, tintVal, 0.25f); foreach (OctreeObject obj in objects) { Gizmos.DrawCube(obj.Bounds.center, obj.Bounds.size); } if (children != null) { for (int i = 0; i < 8; i++) { children[i].DrawAllObjects(); } } Gizmos.color = Color.white; } /// /// We can shrink the octree if: /// - This node is >= double minLength in length /// - All objects in the root node are within one octant /// - This node doesn't have children, or does but 7/8 children are empty /// We can also shrink it if there are no objects left at all! /// /// Minimum dimensions of a node in this octree. /// The new root, or the existing one if we didn't shrink. public BoundsOctreeNode ShrinkIfPossible(float minLength) { if (BaseLength < (2 * minLength)) { return this; } if (objects.Count == 0 && (children == null || children.Length == 0)) { return this; } // Check objects in root int bestFit = -1; for (int i = 0; i < objects.Count; i++) { OctreeObject curObj = objects[i]; int newBestFit = BestFitChild(curObj.Bounds); if (i == 0 || newBestFit == bestFit) { // In same octant as the other(s). Does it fit completely inside that octant? if (Encapsulates(childBounds[newBestFit], curObj.Bounds)) { if (bestFit < 0) { bestFit = newBestFit; } } else { // Nope, so we can't reduce. Otherwise we continue return this; } } else { return this; // Can't reduce - objects fit in different octants } } // Check objects in children if there are any if (children != null) { bool childHadContent = false; for (int i = 0; i < children.Length; i++) { if (children[i].HasAnyObjects()) { if (childHadContent) { return this; // Can't shrink - another child had content already } if (bestFit >= 0 && bestFit != i) { return this; // Can't reduce - objects in root are in a different octant to objects in child } childHadContent = true; bestFit = i; } } } // Can reduce if (children == null) { // We don't have any children, so just shrink this node to the new size // We already know that everything will still fit in it SetValues(BaseLength / 2, minSize, looseness, childBounds[bestFit].center); return this; } // No objects in entire octree if (bestFit == -1) { return this; } // We have children. Use the appropriate child as the new root node return children[bestFit]; } /* /// /// Get the total amount of objects in this node and all its children, grandchildren etc. Useful for debugging. /// /// Used by recursive calls to add to the previous total. /// Total objects in this node and its children, grandchildren etc. public int GetTotalObjects(int startingNum = 0) { int totalObjects = startingNum + objects.Count; if (children != null) { for (int i = 0; i < 8; i++) { totalObjects += children[i].GetTotalObjects(); } } return totalObjects; } */ // #### PRIVATE METHODS #### /// /// Set values for this node. /// /// Length of this node, not taking looseness into account. /// Minimum size of nodes in this octree. /// Multiplier for baseLengthVal to get the actual size. /// Centre position of this node. void SetValues(float baseLengthVal, float minSizeVal, float loosenessVal, Vector3 centerVal) { BaseLength = baseLengthVal; minSize = minSizeVal; looseness = loosenessVal; Center = centerVal; adjLength = looseness * baseLengthVal; // Create the bounding box. Vector3 size = new Vector3(adjLength, adjLength, adjLength); bounds = new Bounds(Center, size); float quarter = BaseLength / 4f; float childActualLength = (BaseLength / 2) * looseness; Vector3 childActualSize = new Vector3(childActualLength, childActualLength, childActualLength); childBounds = new Bounds[8]; childBounds[0] = new Bounds(Center + new Vector3(-quarter, quarter, -quarter), childActualSize); childBounds[1] = new Bounds(Center + new Vector3(quarter, quarter, -quarter), childActualSize); childBounds[2] = new Bounds(Center + new Vector3(-quarter, quarter, quarter), childActualSize); childBounds[3] = new Bounds(Center + new Vector3(quarter, quarter, quarter), childActualSize); childBounds[4] = new Bounds(Center + new Vector3(-quarter, -quarter, -quarter), childActualSize); childBounds[5] = new Bounds(Center + new Vector3(quarter, -quarter, -quarter), childActualSize); childBounds[6] = new Bounds(Center + new Vector3(-quarter, -quarter, quarter), childActualSize); childBounds[7] = new Bounds(Center + new Vector3(quarter, -quarter, quarter), childActualSize); } /// /// Private counterpart to the public Add method. /// /// Object to add. /// 3D bounding box around the object. void SubAdd(T obj, Bounds objBounds) { // We know it fits at this level if we've got this far // Just add if few objects are here, or children would be below min size if (objects.Count < numObjectsAllowed || (BaseLength / 2) < minSize) { OctreeObject newObj = new OctreeObject {Obj = obj, Bounds = objBounds}; //Debug.Log("ADD " + obj.name + " to depth " + depth); objects.Add(newObj); } else { // Fits at this level, but we can go deeper. Would it fit there? // Create the 8 children int bestFitChild; if (children == null) { Split(); if (children == null) { Debug.Log("Child creation failed for an unknown reason. Early exit."); return; } // Now that we have the new children, see if this node's existing objects would fit there for (int i = objects.Count - 1; i >= 0; i--) { OctreeObject existingObj = objects[i]; // Find which child the object is closest to based on where the // object's center is located in relation to the octree's center. bestFitChild = BestFitChild(existingObj.Bounds); // Does it fit? if (Encapsulates(children[bestFitChild].bounds, existingObj.Bounds)) { children[bestFitChild] .SubAdd(existingObj.Obj, existingObj.Bounds); // Go a level deeper objects.Remove(existingObj); // Remove from here } } } // Now handle the new object we're adding now bestFitChild = BestFitChild(objBounds); if (Encapsulates(children[bestFitChild].bounds, objBounds)) { children[bestFitChild].SubAdd(obj, objBounds); } else { OctreeObject newObj = new OctreeObject {Obj = obj, Bounds = objBounds}; //Debug.Log("ADD " + obj.name + " to depth " + depth); objects.Add(newObj); } } } /// /// Private counterpart to the public method. /// /// Object to remove. /// 3D bounding box around the object. /// True if the object was removed successfully. bool SubRemove(T obj, Bounds objBounds) { bool removed = false; for (int i = 0; i < objects.Count; i++) { if (objects[i].Obj.Equals(obj)) { removed = objects.Remove(objects[i]); break; } } if (!removed && children != null) { int bestFitChild = BestFitChild(objBounds); removed = children[bestFitChild].SubRemove(obj, objBounds); } if (removed && children != null) { // Check if we should merge nodes now that we've removed an item if (ShouldMerge()) { Merge(); } } return removed; } /// /// Splits the octree into eight children. /// void Split() { float quarter = BaseLength / 4f; float newLength = BaseLength / 2; children = new BoundsOctreeNode[8]; children[0] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(-quarter, quarter, -quarter)); children[1] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(quarter, quarter, -quarter)); children[2] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(-quarter, quarter, quarter)); children[3] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(quarter, quarter, quarter)); children[4] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(-quarter, -quarter, -quarter)); children[5] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(quarter, -quarter, -quarter)); children[6] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(-quarter, -quarter, quarter)); children[7] = new BoundsOctreeNode(newLength, minSize, looseness, Center + new Vector3(quarter, -quarter, quarter)); } /// /// Merge all children into this node - the opposite of Split. /// Note: We only have to check one level down since a merge will never happen if the children already have children, /// since THAT won't happen unless there are already too many objects to merge. /// void Merge() { // Note: We know children != null or we wouldn't be merging for (int i = 0; i < 8; i++) { BoundsOctreeNode curChild = children[i]; int numObjects = curChild.objects.Count; for (int j = numObjects - 1; j >= 0; j--) { OctreeObject curObj = curChild.objects[j]; objects.Add(curObj); } } // Remove the child nodes (and the objects in them - they've been added elsewhere now) children = null; } /// /// Checks if outerBounds encapsulates innerBounds. /// /// Outer bounds. /// Inner bounds. /// True if innerBounds is fully encapsulated by outerBounds. static bool Encapsulates(Bounds outerBounds, Bounds innerBounds) { return outerBounds.Contains(innerBounds.min) && outerBounds.Contains(innerBounds.max); } /// /// Find which child node this object would be most likely to fit in. /// /// The object's bounds. /// One of the eight child octants. int BestFitChild(Bounds objBounds) { return (objBounds.center.x <= Center.x ? 0 : 1) + (objBounds.center.y >= Center.y ? 0 : 4) + (objBounds.center.z <= Center.z ? 0 : 2); } /// /// Checks if there are few enough objects in this node and its children that the children should all be merged into this. /// /// True there are less or the same abount of objects in this and its children than numObjectsAllowed. bool ShouldMerge() { int totalObjects = objects.Count; if (children != null) { foreach (BoundsOctreeNode child in children) { if (child.children != null) { // If any of the *children* have children, there are definitely too many to merge, // or the child woudl have been merged already return false; } totalObjects += child.objects.Count; } } return totalObjects <= numObjectsAllowed; } /// /// Checks if this node or anything below it has something in it. /// /// True if this node or any of its children, grandchildren etc have something in them public bool HasAnyObjects() { if (objects.Count > 0) return true; if (children != null) { for (int i = 0; i < 8; i++) { if (children[i].HasAnyObjects()) return true; } } return false; } } }