diff --git a/Assets/Data Structures/Octree/BoundsOctree.cs b/Assets/Data Structures/Octree/BoundsOctree.cs new file mode 100644 index 0000000..88d2da9 --- /dev/null +++ b/Assets/Data Structures/Octree/BoundsOctree.cs @@ -0,0 +1,56 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public class BoundsOctree : OctreeBase where T : IHaveBounds + { + public BoundsOctree(Vector3 center, float size, int maxObjects = 8) : base(center, size, maxObjects) + { + } + + public override void Add(T item) + { + root.Add(item); + } + + public override bool Remove(T item) + { + return root.Remove(item); + } + + protected override bool DoesItemIntersectBounds(T item, Bounds bounds) + { + return item.Bounds.Intersects(bounds); + } + + protected override bool IsItemContainedInBounds(T item, Bounds bounds) + { + return bounds.Contains(item.Bounds.min) && bounds.Contains(item.Bounds.max); + } + + protected override float GetItemSqrDistance(T item, Vector3 point) + { + return item.Bounds.SqrDistance(point); + } + + protected override bool DoesItemIntersectRay(T item, Ray ray) + { + return item.Bounds.IntersectRay(ray); + } + + protected override bool DoesItemIntersectSphere(T item, Vector3 center, float radius) + { + return (item.Bounds.ClosestPoint(center) - center).sqrMagnitude <= radius * radius; + } + + protected override bool IsItemInFrustum(T item, Plane[] planes) + { + return GeometryUtility.TestPlanesAABB(planes, item.Bounds); + } + + protected override Bounds GetItemBounds(T item) + { + return item.Bounds; + } + } +} diff --git a/Assets/Data Structures/Octree/BoundsOctreeVisualizer.cs b/Assets/Data Structures/Octree/BoundsOctreeVisualizer.cs new file mode 100644 index 0000000..1c9d459 --- /dev/null +++ b/Assets/Data Structures/Octree/BoundsOctreeVisualizer.cs @@ -0,0 +1,45 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public class BoundsOctreeVisualizer : OctreeVisualizer + { + // A simple example object that has bounds. + private class TestObject : IHaveBounds + { + public Bounds Bounds { get; } + public TestObject(Vector3 center, Vector3 size) + { + Bounds = new Bounds(center, size); + } + } + + public BoundsOctree octree; + + // Example usage: + // void Awake() + // { + // octree = new BoundsOctree(Vector3.zero, 100); + // for (int i = 0; i < 500; i++) + // { + // var center = Random.insideUnitSphere * 40; + // var size = Vector3.one * Random.Range(1, 5); + // octree.Add(new TestObject(center, size)); + // } + // } + + protected override void DrawBoundsGizmos() + { + if (octree == null) return; + Gizmos.color = Color.white; + octree.DrawAllBounds(maxDrawingDepth); + } + + protected override void DrawObjectsGizmos() + { + if (octree == null) return; + Gizmos.color = Color.cyan; + octree.DrawAllObjects(maxDrawingDepth); + } + } +} diff --git a/Assets/Data Structures/Octree/IHaveBounds.cs b/Assets/Data Structures/Octree/IHaveBounds.cs new file mode 100644 index 0000000..bb2fd6f --- /dev/null +++ b/Assets/Data Structures/Octree/IHaveBounds.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public interface IHaveBounds + { + Bounds Bounds { get; } + } +} diff --git a/Assets/Data Structures/Octree/IOctreeObject.cs b/Assets/Data Structures/Octree/IOctreeObject.cs new file mode 100644 index 0000000..e69de29 diff --git a/Assets/Data Structures/Octree/MeshOctree.cs b/Assets/Data Structures/Octree/MeshOctree.cs new file mode 100644 index 0000000..e7fda1e --- /dev/null +++ b/Assets/Data Structures/Octree/MeshOctree.cs @@ -0,0 +1,112 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public struct MeshRaycastHit + { + public float Distance { get; } + public Vector3 Point { get; } + public MeshTriangle Triangle { get; } + public GameObject ParentObject => Triangle.ParentObject; + + public MeshRaycastHit(float distance, Vector3 point, MeshTriangle triangle) + { + Distance = distance; + Point = point; + Triangle = triangle; + } + } + + public class MeshOctree : BoundsOctree + { + public MeshOctree(Vector3 center, float size, int maxObjects = 8) : base(center, size, maxObjects) + { + } + + public void AddGameObject(GameObject go) + { + var meshFilters = go.GetComponentsInChildren(); + foreach (var mf in meshFilters) + { + var mesh = mf.sharedMesh; + if (mesh == null) continue; + + var vertices = mesh.vertices; + var triangles = mesh.triangles; + var transform = mf.transform; + + for (int i = 0; i < triangles.Length; i += 3) + { + Vector3 v1 = transform.TransformPoint(vertices[triangles[i]]); + Vector3 v2 = transform.TransformPoint(vertices[triangles[i + 1]]); + Vector3 v3 = transform.TransformPoint(vertices[triangles[i + 2]]); + + Add(new MeshTriangle(go, v1, v2, v3)); + } + } + } + + public bool Raycast(Ray ray, out MeshRaycastHit hit) + { + hit = default; + float minDistance = float.MaxValue; + + var candidates = GetObjectsAlongRay(ray); + if (candidates.Count == 0) return false; + + foreach (var triangle in candidates) + { + if (RayIntersectsTriangle(ray, triangle, out float distance, out Vector3 point)) + { + if (distance < minDistance) + { + minDistance = distance; + hit = new MeshRaycastHit(distance, point, triangle); + } + } + } + + return minDistance != float.MaxValue; + } + + // Möller–Trumbore intersection algorithm + private bool RayIntersectsTriangle(Ray ray, MeshTriangle triangle, out float distance, out Vector3 hitPoint) + { + distance = 0; + hitPoint = Vector3.zero; + + const float Epsilon = 1e-8f; + Vector3 edge1 = triangle.V2 - triangle.V1; + Vector3 edge2 = triangle.V3 - triangle.V1; + Vector3 h = Vector3.Cross(ray.direction, edge2); + float a = Vector3.Dot(edge1, h); + + if (a > -Epsilon && a < Epsilon) + return false; // Ray is parallel to the triangle + + float f = 1.0f / a; + Vector3 s = ray.origin - triangle.V1; + float u = f * Vector3.Dot(s, h); + + if (u < 0.0f || u > 1.0f) + return false; + + Vector3 q = Vector3.Cross(s, edge1); + float v = f * Vector3.Dot(ray.direction, q); + + if (v < 0.0f || u + v > 1.0f) + return false; + + float t = f * Vector3.Dot(edge2, q); + + if (t > Epsilon) // Ray intersection + { + distance = t; + hitPoint = ray.origin + ray.direction * t; + return true; + } + + return false; + } + } +} diff --git a/Assets/Data Structures/Octree/MeshOctreeVisualizer.cs b/Assets/Data Structures/Octree/MeshOctreeVisualizer.cs new file mode 100644 index 0000000..c571809 --- /dev/null +++ b/Assets/Data Structures/Octree/MeshOctreeVisualizer.cs @@ -0,0 +1,47 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public class MeshOctreeVisualizer : OctreeVisualizer + { + [Tooltip("The target GameObject to build the MeshOctree from.")] + public GameObject targetObject; + + public MeshOctree octree; + + // Example usage: + // void Start() + // { + // if (targetObject != null) + // { + // // Find the bounds of the target object to size the octree correctly. + // var renderers = targetObject.GetComponentsInChildren(); + // if (renderers.Length > 0) + // { + // var totalBounds = renderers[0].bounds; + // for (int i = 1; i < renderers.Length; i++) + // { + // totalBounds.Encapsulate(renderers[i].bounds); + // } + // + // octree = new MeshOctree(totalBounds.center, totalBounds.size.magnitude); + // octree.AddGameObject(targetObject); + // } + // } + // } + + protected override void DrawBoundsGizmos() + { + if (octree == null) return; + Gizmos.color = Color.white; + octree.DrawAllBounds(maxDrawingDepth); + } + + protected override void DrawObjectsGizmos() + { + if (octree == null) return; + Gizmos.color = Color.green; + octree.DrawAllObjects(maxDrawingDepth); + } + } +} diff --git a/Assets/Data Structures/Octree/MeshTriangle.cs b/Assets/Data Structures/Octree/MeshTriangle.cs new file mode 100644 index 0000000..cb39010 --- /dev/null +++ b/Assets/Data Structures/Octree/MeshTriangle.cs @@ -0,0 +1,25 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public struct MeshTriangle : IHaveBounds + { + public GameObject ParentObject { get; } + public Vector3 V1 { get; } + public Vector3 V2 { get; } + public Vector3 V3 { get; } + public Bounds Bounds { get; } + + public MeshTriangle(GameObject parent, Vector3 v1, Vector3 v2, Vector3 v3) + { + ParentObject = parent; + V1 = v1; + V2 = v2; + V3 = v3; + + Vector3 min = Vector3.Min(v1, Vector3.Min(v2, v3)); + Vector3 max = Vector3.Max(v1, Vector3.Max(v2, v3)); + Bounds = new Bounds((min + max) / 2, max - min); + } + } +} diff --git a/Assets/Data Structures/Octree/Octree.cs b/Assets/Data Structures/Octree/Octree.cs new file mode 100644 index 0000000..e69de29 diff --git a/Assets/Data Structures/Octree/OctreeBase.cs b/Assets/Data Structures/Octree/OctreeBase.cs new file mode 100644 index 0000000..24a1f76 --- /dev/null +++ b/Assets/Data Structures/Octree/OctreeBase.cs @@ -0,0 +1,360 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public abstract class OctreeBase + { + protected readonly OctreeNode root; + protected readonly int maxObjectsPerNode; + + protected abstract bool DoesItemIntersectBounds(T item, Bounds bounds); + protected abstract bool IsItemContainedInBounds(T item, Bounds bounds); + protected abstract float GetItemSqrDistance(T item, Vector3 point); + protected abstract bool DoesItemIntersectRay(T item, Ray ray); + protected abstract bool DoesItemIntersectSphere(T item, Vector3 center, float radius); + protected abstract bool IsItemInFrustum(T item, Plane[] planes); + protected abstract Bounds GetItemBounds(T item); + + protected OctreeBase(Vector3 center, float size, int maxObjects) + { + root = new OctreeNode(new Bounds(center, Vector3.one * size), 0, this); + this.maxObjectsPerNode = maxObjects; + } + + public void Add(T item) + { + root.Add(item); + } + + public bool Remove(T item) + { + return root.Remove(item); + } + + public List GetObjectsInBounds(Bounds bounds) + { + var result = new List(); + root.GetObjectsInBounds(bounds, result); + return result; + } + + public bool GetClosestObject(Vector3 point, out T closestObject) + { + float minSqrDistance = float.MaxValue; + closestObject = default(T); + root.GetClosestObject(point, ref closestObject, ref minSqrDistance); + return !EqualityComparer.Default.Equals(closestObject, default(T)); + } + + public List GetObjectsAlongRay(Ray ray) + { + var result = new List(); + root.GetObjectsAlongRay(ray, result); + return result; + } + + public List GetObjectsInSphere(Vector3 center, float radius) + { + var result = new List(); + root.GetObjectsInSphere(center, radius, result); + return result; + } + + public List GetObjectsInFrustum(Plane[] frustumPlanes) + { + var result = new List(); + root.GetObjectsInFrustum(frustumPlanes, result); + return result; + } + + public void DrawAllBounds(int maxDepth = -1) + { + root.DrawNodeBounds(maxDepth); + } + + public void DrawAllObjects(int maxDepth = -1) + { + root.DrawNodeObjects(maxDepth); + } + + protected class OctreeNode + { + public Bounds NodeBounds { get; } + public int Level { get; } + + private readonly List objects = new List(); + private OctreeNode[] children; + private bool IsLeaf => children == null; + private readonly OctreeBase octree; + + public OctreeNode(Bounds bounds, int level, OctreeBase octree) + { + this.NodeBounds = bounds; + this.Level = level; + this.octree = octree; + } + + public void Add(T item) + { + if (!octree.DoesItemIntersectBounds(item, NodeBounds)) return; + + if (IsLeaf) + { + objects.Add(item); + if (objects.Count > octree.maxObjectsPerNode && Level < 10) // Add a depth limit to prevent infinite subdivision + { + Subdivide(); + } + } + else + { + int index = GetChildIndex(item); + if (index != -1) + { + children[index].Add(item); + } + else + { + objects.Add(item); + } + } + } + + public bool Remove(T item) + { + if (!octree.DoesItemIntersectBounds(item, NodeBounds)) return false; + + if (objects.Remove(item)) return true; + + if (!IsLeaf) + { + int index = GetChildIndex(item); + if (index != -1) + { + if (children[index].Remove(item)) + { + TryCollapse(); + return true; + } + } + } + return false; + } + + public void GetObjectsInBounds(Bounds bounds, List result) + { + if (!NodeBounds.Intersects(bounds)) return; + + foreach (var obj in objects) + { + if (octree.DoesItemIntersectBounds(obj, bounds)) + { + result.Add(obj); + } + } + + if (!IsLeaf) + { + foreach (var child in children) + { + child.GetObjectsInBounds(bounds, result); + } + } + } + + public void GetClosestObject(Vector3 point, ref T closestObject, ref float minSqrDistance) + { + if (NodeBounds.SqrDistance(point) > minSqrDistance) return; + + foreach (var obj in objects) + { + float sqrDistance = octree.GetItemSqrDistance(obj, point); + if (sqrDistance < minSqrDistance) + { + minSqrDistance = sqrDistance; + closestObject = obj; + } + } + + if (!IsLeaf) + { + var childOrder = GetChildIndicesByDistance(point); + foreach (var index in childOrder) + { + children[index].GetClosestObject(point, ref closestObject, ref minSqrDistance); + } + } + } + + public void GetObjectsAlongRay(Ray ray, List result) + { + if (!NodeBounds.IntersectRay(ray)) return; + + foreach (var obj in objects) + { + if (octree.DoesItemIntersectRay(obj, ray)) + { + result.Add(obj); + } + } + + if (!IsLeaf) + { + foreach (var child in children) + { + child.GetObjectsAlongRay(ray, result); + } + } + } + + public void GetObjectsInSphere(Vector3 center, float radius, List result) + { + if ((NodeBounds.ClosestPoint(center) - center).sqrMagnitude > radius * radius) return; + + foreach (var obj in objects) + { + if (octree.DoesItemIntersectSphere(obj, center, radius)) + { + result.Add(obj); + } + } + + if (!IsLeaf) + { + foreach (var child in children) + { + child.GetObjectsInSphere(center, radius, result); + } + } + } + + public void GetObjectsInFrustum(Plane[] frustumPlanes, List result) + { + if (!GeometryUtility.TestPlanesAABB(frustumPlanes, NodeBounds)) return; + + foreach (var obj in objects) + { + if (octree.IsItemInFrustum(obj, frustumPlanes)) + { + result.Add(obj); + } + } + + if (!IsLeaf) + { + foreach (var child in children) + { + child.GetObjectsInFrustum(frustumPlanes, result); + } + } + } + + public void DrawNodeBounds(int maxDepth) + { + if (maxDepth != -1 && Level > maxDepth) return; + + Gizmos.DrawWireCube(NodeBounds.center, NodeBounds.size); + + if (!IsLeaf) + { + foreach (var child in children) + { + child.DrawNodeBounds(maxDepth); + } + } + } + + public void DrawNodeObjects(int maxDepth) + { + if (maxDepth != -1 && Level > maxDepth) return; + + foreach (var obj in objects) + { + Gizmos.DrawWireCube(octree.GetItemBounds(obj).center, octree.GetItemBounds(obj).size); + } + if (!IsLeaf) + { + foreach (var child in children) + { + child.DrawNodeObjects(maxDepth); + } + } + } + + private int[] GetChildIndicesByDistance(Vector3 point) + { + int[] indices = { 0, 1, 2, 3, 4, 5, 6, 7 }; + System.Array.Sort(indices, (a, b) => + children[a].NodeBounds.SqrDistance(point).CompareTo( + children[b].NodeBounds.SqrDistance(point))); + return indices; + } + + private int GetChildIndex(T item) + { + int index = -1; + for (int i = 0; i < 8; i++) + { + if (children[i] == null) continue; + + if (octree.IsItemContainedInBounds(item, children[i].NodeBounds)) + { + if (index != -1) return -1; + index = i; + } + } + return index; + } + + private void Subdivide() + { + children = new OctreeNode[8]; + float childSize = NodeBounds.size.y / 2.0f; + var childSizeVec = new Vector3(childSize, childSize, childSize); + Vector3 parentCenter = NodeBounds.center; + + for (int i = 0; i < 8; i++) + { + Vector3 childCenter = parentCenter; + childCenter.x += (i & 1) == 0 ? -childSize / 2.0f : childSize / 2.0f; + childCenter.y += (i & 2) == 0 ? -childSize / 2.0f : childSize / 2.0f; + childCenter.z += (i & 4) == 0 ? -childSize / 2.0f : childSize / 2.0f; + children[i] = new OctreeNode(new Bounds(childCenter, childSizeVec), Level + 1, octree); + } + + for (int i = objects.Count - 1; i >= 0; i--) + { + T obj = objects[i]; + int index = GetChildIndex(obj); + if (index != -1) + { + children[index].Add(obj); + objects.RemoveAt(i); + } + } + } + + private void TryCollapse() + { + if (IsLeaf) return; + + int totalObjects = objects.Count; + foreach (var child in children) + { + if (!child.IsLeaf) return; + totalObjects += child.objects.Count; + } + + if (totalObjects <= octree.maxObjectsPerNode) + { + foreach (var child in children) + { + objects.AddRange(child.objects); + } + children = null; + } + } + } + } +} diff --git a/Assets/Data Structures/Octree/OctreeVisualizer.cs b/Assets/Data Structures/Octree/OctreeVisualizer.cs new file mode 100644 index 0000000..609b322 --- /dev/null +++ b/Assets/Data Structures/Octree/OctreeVisualizer.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public abstract class OctreeVisualizer : MonoBehaviour + { + [Tooltip("Toggle drawing of the Octree node bounds.")] + public bool drawNodeBounds = true; + + [Tooltip("Toggle drawing of the object bounds within the Octree.")] + public bool drawNodeObjects = true; + + [Tooltip("The maximum depth to draw the Octree to. -1 means no limit.")] + public int maxDrawingDepth = -1; + + private void OnDrawGizmos() + { + if (drawNodeBounds) + { + DrawBoundsGizmos(); + } + + if (drawNodeObjects) + { + DrawObjectsGizmos(); + } + } + + protected abstract void DrawBoundsGizmos(); + protected abstract void DrawObjectsGizmos(); + } +} diff --git a/Assets/Data Structures/Octree/PointOctree.cs b/Assets/Data Structures/Octree/PointOctree.cs new file mode 100644 index 0000000..ac91c11 --- /dev/null +++ b/Assets/Data Structures/Octree/PointOctree.cs @@ -0,0 +1,65 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public class PointOctree : OctreeBase + { + public PointOctree(Vector3 center, float size, int maxObjects = 8) : base(center, size, maxObjects) + { + } + + public override void Add(Vector3 point) + { + root.Add(point); + } + + public override bool Remove(Vector3 point) + { + return root.Remove(point); + } + + protected override bool DoesItemIntersectBounds(Vector3 point, Bounds bounds) + { + return bounds.Contains(point); + } + + protected override bool IsItemContainedInBounds(Vector3 point, Bounds bounds) + { + return bounds.Contains(point); + } + + protected override float GetItemSqrDistance(Vector3 point, Vector3 otherPoint) + { + return (point - otherPoint).sqrMagnitude; + } + + protected override bool DoesItemIntersectRay(Vector3 point, Ray ray) + { + float sqrDist = Vector3.Cross(ray.direction, point - ray.origin).sqrMagnitude; + const float Epsilon = 1e-6f; + return sqrDist < Epsilon; + } + + protected override bool DoesItemIntersectSphere(Vector3 point, Vector3 center, float radius) + { + return (point - center).sqrMagnitude <= radius * radius; + } + + protected override bool IsItemInFrustum(Vector3 point, Plane[] planes) + { + foreach (var plane in planes) + { + if (plane.GetSide(point) == false) + { + return false; + } + } + return true; + } + + protected override Bounds GetItemBounds(Vector3 point) + { + return new Bounds(point, Vector3.one * 0.1f); // Represent point as a small cube + } + } +} diff --git a/Assets/Data Structures/Octree/PointOctreeVisualizer.cs b/Assets/Data Structures/Octree/PointOctreeVisualizer.cs new file mode 100644 index 0000000..c39b99e --- /dev/null +++ b/Assets/Data Structures/Octree/PointOctreeVisualizer.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace GG.DataStructures.Octree +{ + public class PointOctreeVisualizer : OctreeVisualizer + { + public PointOctree octree; + + // Example usage: + // void Awake() + // { + // octree = new PointOctree(Vector3.zero, 100); + // for (int i = 0; i < 1000; i++) + // { + // octree.Add(Random.insideUnitSphere * 50); + // } + // } + + protected override void DrawBoundsGizmos() + { + if (octree == null) return; + Gizmos.color = Color.white; + octree.DrawAllBounds(maxDrawingDepth); + } + + protected override void DrawObjectsGizmos() + { + if (octree == null) return; + Gizmos.color = Color.yellow; + octree.DrawAllObjects(maxDrawingDepth); + } + } +} diff --git a/Assets/Tests/DataStructures/BoundsOctreeTests.cs b/Assets/Tests/DataStructures/BoundsOctreeTests.cs new file mode 100644 index 0000000..549e59c --- /dev/null +++ b/Assets/Tests/DataStructures/BoundsOctreeTests.cs @@ -0,0 +1,57 @@ +using GG.DataStructures.Octree; +using NUnit.Framework; +using UnityEngine; + +namespace GG.DataStructures.Tests +{ + public class BoundsOctreeTests + { + private struct TestBoundsObject : IHaveBounds + { + public Bounds Bounds { get; set; } + public int ID { get; set; } + + public override bool Equals(object obj) => obj is TestBoundsObject other && other.ID == ID; + public override int GetHashCode() => ID; + } + + [Test] + public void Add_SingleObject_CanBeFound() + { + var octree = new BoundsOctree(Vector3.zero, 10); + var obj = new TestBoundsObject { Bounds = new Bounds(new Vector3(1, 1, 1), Vector3.one), ID = 1 }; + octree.Add(obj); + + var results = octree.GetObjectsInBounds(new Bounds(new Vector3(1, 1, 1), Vector3.one * 2)); + Assert.IsTrue(results.Contains(obj)); + } + + [Test] + public void Remove_SingleObject_IsRemoved() + { + var octree = new BoundsOctree(Vector3.zero, 10); + var obj = new TestBoundsObject { Bounds = new Bounds(new Vector3(1, 1, 1), Vector3.one), ID = 1 }; + octree.Add(obj); + octree.Remove(obj); + + var results = octree.GetObjectsInBounds(new Bounds(new Vector3(1, 1, 1), Vector3.one * 2)); + Assert.IsFalse(results.Contains(obj)); + } + + [Test] + public void GetObjectsAlongRay_FindsCorrectObject() + { + var octree = new BoundsOctree(Vector3.zero, 20); + var obj1 = new TestBoundsObject { Bounds = new Bounds(new Vector3(5, 0, 0), Vector3.one), ID = 1 }; + var obj2 = new TestBoundsObject { Bounds = new Bounds(new Vector3(-5, 0, 0), Vector3.one), ID = 2 }; + octree.Add(obj1); + octree.Add(obj2); + + var ray = new Ray(Vector3.zero, Vector3.right); + var results = octree.GetObjectsAlongRay(ray); + + Assert.IsTrue(results.Contains(obj1)); + Assert.IsFalse(results.Contains(obj2)); + } + } +} diff --git a/Assets/Tests/DataStructures/GG.DataStructures.Tests.asmdef b/Assets/Tests/DataStructures/GG.DataStructures.Tests.asmdef new file mode 100644 index 0000000..7ac5fb6 --- /dev/null +++ b/Assets/Tests/DataStructures/GG.DataStructures.Tests.asmdef @@ -0,0 +1,22 @@ +{ + "name": "GG.DataStructures.Tests", + "rootNamespace": "", + "references": [ + "GG.DataStructures" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Tests/DataStructures/MeshOctreeTests.cs b/Assets/Tests/DataStructures/MeshOctreeTests.cs new file mode 100644 index 0000000..787cf9b --- /dev/null +++ b/Assets/Tests/DataStructures/MeshOctreeTests.cs @@ -0,0 +1,72 @@ +using GG.DataStructures.Octree; +using NUnit.Framework; +using UnityEngine; + +namespace GG.DataStructures.Tests +{ + public class MeshOctreeTests + { + private GameObject CreateTestQuad() + { + var go = new GameObject("TestQuad"); + var mf = go.AddComponent(); + var mesh = new Mesh(); + + mesh.vertices = new[] + { + new Vector3(-1, -1, 0), + new Vector3(1, -1, 0), + new Vector3(-1, 1, 0), + new Vector3(1, 1, 0) + }; + + mesh.triangles = new[] + { + 0, 2, 1, + 2, 3, 1 + }; + + mf.mesh = mesh; + return go; + } + + [Test] + public void AddGameObject_And_Raycast_HitsCorrectObject() + { + // Arrange + var octree = new MeshOctree(Vector3.zero, 20); + var quad = CreateTestQuad(); + octree.AddGameObject(quad); + + // Act + var ray = new Ray(new Vector3(0, 0, -5), Vector3.forward); + bool didHit = octree.Raycast(ray, out var hit); + + // Assert + Assert.IsTrue(didHit); + Assert.AreEqual(quad, hit.ParentObject); + + // Cleanup + Object.DestroyImmediate(quad); + } + + [Test] + public void Raycast_Misses_ReturnsFalse() + { + // Arrange + var octree = new MeshOctree(Vector3.zero, 20); + var quad = CreateTestQuad(); + octree.AddGameObject(quad); + + // Act + var ray = new Ray(new Vector3(5, 5, -5), Vector3.forward); + bool didHit = octree.Raycast(ray, out _); + + // Assert + Assert.IsFalse(didHit); + + // Cleanup + Object.DestroyImmediate(quad); + } + } +} diff --git a/Assets/Tests/DataStructures/PointOctreeTests.cs b/Assets/Tests/DataStructures/PointOctreeTests.cs new file mode 100644 index 0000000..f41f742 --- /dev/null +++ b/Assets/Tests/DataStructures/PointOctreeTests.cs @@ -0,0 +1,45 @@ +using GG.DataStructures.Octree; +using NUnit.Framework; +using UnityEngine; + +namespace GG.DataStructures.Tests +{ + public class PointOctreeTests + { + [Test] + public void Add_SinglePoint_CanBeFound() + { + var octree = new PointOctree(Vector3.zero, 10); + var point = new Vector3(1, 2, 3); + octree.Add(point); + + octree.GetClosestObject(point, out var closest); + Assert.AreEqual(point, closest); + } + + [Test] + public void Remove_SinglePoint_IsRemoved() + { + var octree = new PointOctree(Vector3.zero, 10); + var point = new Vector3(1, 2, 3); + octree.Add(point); + octree.Remove(point); + + bool found = octree.GetClosestObject(point, out _); + Assert.IsFalse(found); + } + + [Test] + public void GetClosestObject_FindsCorrectPoint() + { + var octree = new PointOctree(Vector3.zero, 10); + var p1 = new Vector3(1, 1, 1); + var p2 = new Vector3(4, 4, 4); + octree.Add(p1); + octree.Add(p2); + + octree.GetClosestObject(Vector3.zero, out var closest); + Assert.AreEqual(p1, closest); + } + } +}