diff --git a/Assets/AABB2D.cs b/Assets/AABB2D.cs index 83735b9..2551e9a 100644 --- a/Assets/AABB2D.cs +++ b/Assets/AABB2D.cs @@ -1,59 +1,103 @@ using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using PlasticGui.WorkspaceWindow.Items; using Unity.Mathematics; +using UnityEngine; namespace NativeQuadTree { - [Serializable] - public struct AABB2D { - public float2 Center; - public float2 Extents; - - public float2 Size => Extents * 2; - public float2 Min => Center - Extents; - public float2 Max => Center + Extents; - - public AABB2D(float2 center, float2 extents) - { - Center = center; - Extents = extents; - } - - public bool Contains(float2 point) { - if (point[0] < Center[0] - Extents[0]) { - return false; - } - if (point[0] > Center[0] + Extents[0]) { - return false; - } - - if (point[1] < Center[1] - Extents[1]) { - return false; - } - if (point[1] > Center[1] + Extents[1]) { - return false; - } - - return true; - } - - public bool Contains(AABB2D b) { - return Contains(b.Center + new float2(-b.Extents.x, -b.Extents.y)) && - Contains(b.Center + new float2(-b.Extents.x, b.Extents.y)) && - Contains(b.Center + new float2(b.Extents.x, -b.Extents.y)) && - Contains(b.Center + new float2(b.Extents.x, b.Extents.y)); - } - - public bool Intersects(AABB2D b) - { - //bool noOverlap = Min[0] > b.Max[0] || - // b.Min[0] > Max[0]|| - // Min[1] > b.Max[1] || - // b.Min[1] > Max[1]; + [Serializable, DebuggerDisplay("Center: {Center}, Extents: {Extents}")] + public struct AABB2D + { + public float2 Center; + public float2 Extents; + + public float2 Size => Extents * 2; + public float2 Min => Center - Extents; + public float2 Max => Center + Extents; + + public AABB2D(float2 center, float2 extents) + { + Center = center; + Extents = extents; + } + + public AABB2D(RectTransform rect) + { + Center = new float2(rect.position.x, rect.position.y); + Extents = new float2((rect.rect.max - rect.rect.min) / 2f); + } + + public bool Contains(float2 point) + { + if(point.x < Center.x - Extents.x) + return false; + + if(point.x > Center.x + Extents.x) + return false; + + if(point.y < Center.y - Extents.y) + return false; + + if(point.y > Center.y + Extents.y) + return false; + + return true; + } + + public bool Contains(AABB2D b) + { + return Contains(b.Center + -b.Extents) && + Contains(b.Center + new float2(-b.Extents.x, b.Extents.y)) && + Contains(b.Center + new float2(b.Extents.x, -b.Extents.y)) && + Contains(b.Center + b.Extents); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Circle2D b) + { + if(Contains(b.Center)) + { + // inside box + float2 squareEdgePoint = math.clamp(b.Center, Center - Extents, Center + Extents); + float distance = math.distance(squareEdgePoint, Center); + + if(distance + b.Radious <= math.max(Extents.x, Extents.y)) + { + return true; + } + else + { + // this could mean that the point is in the very corner of the square + float BL = math.distance(b.Center, Center + -Extents); + float TL = math.distance(b.Center, Center + new float2(-Extents.x, Extents.y)); + float BR = math.distance(b.Center, Center + new float2(Extents.x, -Extents.y)); + float TR = math.distance(b.Center, Center + Extents.x); + float closestCornerDistance = math.min(math.min(BL, TL), math.min(BR, TR)); + return closestCornerDistance > b.Radious; + } + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Intersects(Circle2D b) + { + return Circle2D.Intersects(this, b); + } + + public bool Intersects(AABB2D b) + { + //bool noOverlap = Min[0] > b.Max[0] || + // b.Min[0] > Max[0]|| + // Min[1] > b.Max[1] || + // b.Min[1] > Max[1]; // - //return !noOverlap; + //return !noOverlap; - return (math.abs(Center[0] - b.Center[0]) < (Extents[0] + b.Extents[0])) && - (math.abs(Center[1] - b.Center[1]) < (Extents[1] + b.Extents[1])); - } - } + return (math.abs(Center.x - b.Center.x) < (Extents.x + b.Extents.x)) && + (math.abs(Center.y - b.Center.y) < (Extents.y + b.Extents.y)); + } + } } \ No newline at end of file diff --git a/Assets/Circle2D.cs b/Assets/Circle2D.cs new file mode 100644 index 0000000..99e6fd2 --- /dev/null +++ b/Assets/Circle2D.cs @@ -0,0 +1,62 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using NativeQuadTree; +using Unity.Mathematics; + +namespace NativeQuadTree +{ + public struct Circle2D + { + public float2 Center; + public float Radious; + + public Circle2D(float2 center, float radious) + : this() + { + Center = center; + Radious = radious; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(float2 point) + { + return math.distance(point, Center) <= Radious; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(AABB2D b) + { + // check that all 4 points are inside circle + return Contains(b.Center + -b.Extents) && + Contains(b.Center + new float2(-b.Extents.x, b.Extents.y)) && + Contains(b.Center + new float2(b.Extents.x, -b.Extents.y)) && + Contains(b.Center + b.Extents); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Intersects(AABB2D a) + { + return Intersects(a, this); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool Intersects(AABB2D a, Circle2D b) + { + float2 squareEdgePoint = math.clamp(b.Center, a.Center - a.Extents, a.Center + a.Extents); + float distance = math.distance(squareEdgePoint, b.Center); + + if(a.Contains(b.Center)) + { + // inside box + /*float length = math.max(a.Extents.x, a.Extents.y); + return distance > b.Radious || length < b.Radious;*/ + return true; + } + else + { + // outside box + return distance < b.Radious; + } + } + } +} \ No newline at end of file diff --git a/Assets/NativeQuadTreeRangeQuery.cs.meta b/Assets/Circle2D.cs.meta similarity index 83% rename from Assets/NativeQuadTreeRangeQuery.cs.meta rename to Assets/Circle2D.cs.meta index 0450437..8949ccc 100644 --- a/Assets/NativeQuadTreeRangeQuery.cs.meta +++ b/Assets/Circle2D.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 98c569dc7c35745c5bf8a8cbca70fb3b +guid: 3c89bcf00d5edb3468e08ad285f3bf44 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Editor/CollisionShapeTests.cs b/Assets/Editor/CollisionShapeTests.cs new file mode 100644 index 0000000..539ce52 --- /dev/null +++ b/Assets/Editor/CollisionShapeTests.cs @@ -0,0 +1,292 @@ +using NativeQuadTree; +using NUnit.Framework; +using Unity.Mathematics; +using Assert = UnityEngine.Assertions.Assert; + +public class CollisionShapeTests +{ + [Test] + public void SquareOverlap() + { + AABB2D largeSquare = new AABB2D(new float2(5f), new float2(5f)); + AABB2D smallSquare = new AABB2D(new float2(5f), new float2(1f)); + + Assert.IsTrue(largeSquare.Contains(smallSquare), "small square is entirely contained inside the large square"); + } + + [Test] + public void SquareNoOverlap() + { + AABB2D square1 = new AABB2D(new float2(5f), new float2(5f)); + AABB2D square2 = new AABB2D(new float2(-2f), new float2(1f)); + AABB2D square3 = new AABB2D(new float2(15f), new float2(6f)); + + Assert.IsFalse(square1.Contains(square2), "No overlap between the two squares"); + Assert.IsFalse(square1.Contains(square3), "No overlap between the two squares"); + Assert.IsFalse(square2.Contains(square3), "No overlap between the two squares"); + } + + [Test] + public void SquareEdgeIntersect() + { + AABB2D largeSquare = new AABB2D(new float2(5f), new float2(5f)); + AABB2D perfectOverlap = new AABB2D(new float2(1f), new float2(1f)); + + Assert.IsTrue(largeSquare.Intersects(perfectOverlap), "perfect overlap is entirely contained inside the large square"); + Assert.IsTrue(perfectOverlap.Intersects(largeSquare), "perfect overlap is entirely contained inside the large square"); + Assert.IsTrue(largeSquare.Intersects(largeSquare), "perfect overlap is entirely contained inside the large square"); + } + + [Test] + public void SquareEdgePartialIntersect() + { + AABB2D largeSquare = new AABB2D(new float2(5f), new float2(5f)); + AABB2D partialOverlap = new AABB2D(new float2(0.8f), new float2(1f)); + + Assert.IsTrue(largeSquare.Intersects(partialOverlap), "partial overlap is contained inside the large square"); + } + + [Test] + public void SquareEdgePartialIntersect2() + { + AABB2D horizontal = new AABB2D(new float2(5f), new float2(5f, 1f)); + AABB2D vertical = new AABB2D(new float2(5), new float2(1f, 5f)); + + Assert.IsTrue(horizontal.Intersects(vertical), "partial overlap without either square containing a corner"); + Assert.IsTrue(vertical.Intersects(horizontal), "partial overlap without either square containing a corner"); + } + + [Test] + public void SquareCircleContains() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 1f); + + Assert.IsTrue(box.Contains(testCircle), "fully enclosed inside square"); + } + + [Test] + public void SquareCircleContains2() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 5f); + + Assert.IsTrue(box.Contains(testCircle), "fully enclosed inside square"); + } + + [Test] + public void SquareCircleContains3() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 8f); + + Assert.IsFalse(box.Contains(testCircle), "the circle is outside the bounds of the square so it isn't fully contained"); + } + + [Test] + public void SquareCircleContains3V2() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(6f, 8f); + + Assert.IsFalse(box.Contains(testCircle), "the circle is outside the bounds of the square so it isn't fully contained"); + } + + [Test] + public void SquareCircleContains4() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(5f, 3f); + + Assert.IsTrue(box.Contains(testCircle), "fully contained"); + } + + [Test] + public void SquareCircleContains5() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(-5f, 3f); + + Assert.IsFalse(box.Contains(testCircle), "outside the square"); + } + + [Test] + public void SquareCircleContains6() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(3.1f, 3f); + + Assert.IsTrue(box.Contains(testCircle), "fully contained at the very edge of the square"); + } + + [Test] + public void SquareCircleIntersect() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(1f, 50f), 3f); + + Assert.IsTrue(box.Intersects(testCircle), "intersects the left side of square with the majority of it's area"); + } + + [Test] + public void SquareCircleIntersect2() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(-1f, 50f), 3f); + + Assert.IsTrue(box.Intersects(testCircle), "intersects the left side of square with the minority of it's area"); + } + + [Test] + public void SquareCircleIntersect3() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 8f); + + Assert.IsTrue(box.Intersects(testCircle), "starts inside the square but expands outside it's bounds"); + } + + [Test] + public void SquareCircleIntersect4() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(50f, 1f), 3f); + + Assert.IsTrue(box.Intersects(testCircle), "intersects the bottom side of square with the majority of it's area"); + } + + [Test] + public void SquareCircleIntersect5() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(50f, -1f), 3f); + + Assert.IsTrue(box.Intersects(testCircle), "intersects the bottom side of square with the minority of it's area"); + } + + [Test] + public void CircleContains() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 1f); + + Assert.IsFalse(testCircle.Contains(box), "fully enclosed inside square, so circle doesn't contain the entire box"); + } + + [Test] + public void CircleContains2() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 5f); + + Assert.IsFalse(testCircle.Contains(box), "circle fully enclosed inside square, so circle doesn't contain the entire box"); + } + + [Test] + public void CircleContains3() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 8f); + + Assert.IsTrue(testCircle.Contains(box), "the circle fully contains the box"); + } + + [Test] + public void CircleContains3V2() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(6f, 8f); + + Assert.IsFalse(testCircle.Contains(box), "the circle is outside the bounds of the square so it isn't fully contained"); + } + + [Test] + public void CircleContains4() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(5f, 3f); + + Assert.IsFalse(testCircle.Contains(box), "box is fully around the circle, so circle can't contain the box"); + } + + [Test] + public void CircleContains5() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(-5f, 3f); + + Assert.IsFalse(testCircle.Contains(box), "outside the square"); + } + + [Test] + public void CircleContains6() + { + Circle2D testCircle = new Circle2D(5f, 10f); + AABB2D box = new AABB2D(new float2(5), new float2(6f)); + + Assert.IsTrue(testCircle.Contains(box), "box is fully contained"); + } + + [Test] + public void CircleContains7() + { + Circle2D testCircle = new Circle2D(5f, 10f); + AABB2D box = new AABB2D(new float2(3), new float2(1f)); + + Assert.IsTrue(testCircle.Contains(box), "box is fully contained"); + } + + [Test] + public void CircleIntersect() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(1f, 50f), 3f); + + Assert.IsTrue(testCircle.Intersects(box), "intersects the left side of square with the majority of it's area"); + } + + [Test] + public void CircleIntersect2() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(-1f, 50f), 3f); + + Assert.IsTrue(testCircle.Intersects(box), "intersects the left side of square with the minority of it's area"); + } + + [Test] + public void CircleIntersect3() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 8f); + + Assert.IsTrue(testCircle.Intersects(box), "starts inside the square but expands outside it's bounds"); + } + + [Test] + public void CircleIntersect4() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(50f, 1f), 3f); + + Assert.IsTrue(testCircle.Intersects(box), "intersects the bottom side of square with the majority of it's area"); + } + + [Test] + public void CircleIntersect5() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(50f, -1f), 3f); + + Assert.IsTrue(testCircle.Intersects(box), "intersects the bottom side of square with the minority of it's area"); + } + + [Test] + public void CircleIntersect6() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(new float2(5f, 5f), 3f); + + Assert.IsTrue(testCircle.Intersects(box), "the circle is fully inside the box which means it intersects the box area... not the edge"); + } +} \ No newline at end of file diff --git a/Assets/Editor/CollisionShapeTests.cs.meta b/Assets/Editor/CollisionShapeTests.cs.meta new file mode 100644 index 0000000..8cbc895 --- /dev/null +++ b/Assets/Editor/CollisionShapeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a03b7b521fa70104d96f0075ecbffa05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/NativeQuadTree.Editor.asmdef b/Assets/Editor/NativeQuadTree.Editor.asmdef new file mode 100644 index 0000000..95821e6 --- /dev/null +++ b/Assets/Editor/NativeQuadTree.Editor.asmdef @@ -0,0 +1,21 @@ +{ + "name": "NativeQuadTree.Editor", + "rootNamespace": "NativeQuadTree.Editor", + "references": [ + "GUID:4259b4454b3b86a4790bd00dd10d9797", + "GUID:d8b63aba1907145bea998dd612889d6b", + "GUID:8a2eafa29b15f444eb6d74f94a930e1d", + "GUID:e0cd26848372d4e5c891c569017e11f1" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Editor/NativeQuadTree.Editor.asmdef.meta b/Assets/Editor/NativeQuadTree.Editor.asmdef.meta new file mode 100644 index 0000000..c860141 --- /dev/null +++ b/Assets/Editor/NativeQuadTree.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c6e02b28d35335f4a80bbbda87e90c2f +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/QuadTreeDrawer.cs b/Assets/Editor/QuadTreeDrawer.cs index 9b154a1..34bded1 100644 --- a/Assets/Editor/QuadTreeDrawer.cs +++ b/Assets/Editor/QuadTreeDrawer.cs @@ -1,4 +1,5 @@ using NativeQuadTree; +using NativeQuadTree.Jobs; using Unity.Collections; using UnityEditor; using UnityEngine; @@ -14,10 +15,12 @@ static void Init() public static void Draw(NativeQuadTree quadTree) where T : unmanaged { QuadTreeDrawer window = (QuadTreeDrawer)GetWindow(typeof(QuadTreeDrawer)); - window.DoDraw(quadTree, default, default); + NativeList> results = new NativeList>(Allocator.TempJob); + window.DoDraw(quadTree, results, default); + results.Dispose(); } - public static void DrawWithResults(QuadTreeJobs.RangeQueryJob queryJob) where T : unmanaged + public static void DrawWithResults(RangeQueryJob queryJob) where T : unmanaged { QuadTreeDrawer window = (QuadTreeDrawer)GetWindow(typeof(QuadTreeDrawer)); window.DoDraw(queryJob); @@ -33,10 +36,10 @@ void DoDraw(NativeQuadTree quadTree, NativeList> results, A { pixels[i] = new Color[256]; } - NativeQuadTree.Draw(quadTree, results, bounds, pixels); + NativeQuadTreeDrawHelpers.Draw(quadTree, results, bounds, pixels); } - void DoDraw(QuadTreeJobs.RangeQueryJob queryJob) where T : unmanaged + void DoDraw(RangeQueryJob queryJob) where T : unmanaged { DoDraw(queryJob.QuadTree, queryJob.Results, queryJob.Bounds); } diff --git a/Assets/Editor/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 79d774b..65270d5 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using NUnit.Framework; using NativeQuadTree; +using NativeQuadTree.Helpers; +using NativeQuadTree.Jobs; using Unity.Burst; using Unity.Collections; using Unity.Jobs; @@ -32,21 +34,23 @@ public void InsertTriggerDivideBulk() { var values = GetValues(); - var elements = new NativeArray>(values.Length, Allocator.TempJob); + var elements = new NativeArray>(values.Length, Allocator.Persistent); for (int i = 0; i < values.Length; i++) { elements[i] = new QuadElement { - pos = values[i], - element = i + Pos = values[i], + Element = i }; } - var job = new QuadTreeJobs.AddBulkJob + NativeReference> data = new NativeReference>(Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + data.Value = new NativeQuadTree(Bounds, maxLeafElements: 1000); + var job = new AddBulkJob { Elements = elements, - QuadTree = new NativeQuadTree(Bounds) + QuadTree = data }; var s = Stopwatch.StartNew(); @@ -56,7 +60,7 @@ public void InsertTriggerDivideBulk() s.Stop(); Debug.Log(s.Elapsed.TotalMilliseconds); - QuadTreeDrawer.Draw(job.QuadTree); + QuadTreeDrawer.Draw(data.Value); job.QuadTree.Dispose(); elements.Dispose(); } @@ -72,15 +76,15 @@ public void RangeQueryAfterBulk() { elements[i] = new QuadElement { - pos = values[i], - element = i + Pos = values[i], + Element = i }; } - var quadTree = new NativeQuadTree(Bounds); + var quadTree = new NativeQuadTree(Bounds, maxLeafElements: 1000); quadTree.ClearAndBulkInsert(elements); - var queryJob = new QuadTreeJobs.RangeQueryJob + var queryJob = new RangeQueryJob { QuadTree = quadTree, Bounds = new AABB2D(100, 140), @@ -103,20 +107,17 @@ public void InsertTriggerDivideNonBurstBulk() { var values = GetValues(); - var positions = new NativeArray(values.Length, Allocator.TempJob); - var quadTree = new NativeQuadTree(Bounds); + var positions = new NativeArray(values, Allocator.Persistent); + var quadTree = new NativeQuadTree(Bounds, maxLeafElements: 1000); - positions.CopyFrom(values); - - - NativeArray> elements = new NativeArray>(positions.Length, Allocator.Temp); + NativeArray> elements = new NativeArray>(positions.Length, Allocator.Persistent); for (int i = 0; i < positions.Length; i++) { elements[i] = new QuadElement { - pos = positions[i], - element = i + Pos = positions[i], + Element = i }; } @@ -128,7 +129,192 @@ public void InsertTriggerDivideNonBurstBulk() Debug.Log(s.Elapsed.TotalMilliseconds); QuadTreeDrawer.Draw(quadTree); + quadTree.Dispose(); positions.Dispose(); + elements.Dispose(); + } + + [Test] + public void SimpleNativeQuery([NUnit.Framework.Range(0, 20)] int count) + { + NativeArray> elements = new NativeArray>(count, Allocator.TempJob); + for (int i = 0; i < count; i++) + { + elements[i] = new QuadElement + { + Pos = new float2(0.1f + (0.02f * i), 1f), + Element = i + }; + } + + const int size = 30; + AABB2D bounds = new AABB2D(new float2(size, 4f), new float2(size, 4f)); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 3, maxLeafElements: 20); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } + + [Test] + public void MultiDepthTree() + { + NativeArray> elements = new NativeArray>(7, Allocator.TempJob); + // nodes can all fit into a single quad + elements[0] = new QuadElement() { Pos = new float2(0.1f, 0f) }; + elements[1] = new QuadElement() { Pos = new float2(0.2f, 0f) }; + elements[2] = new QuadElement() { Pos = new float2(0.3f, 0f) }; + elements[3] = new QuadElement() { Pos = new float2(0.4f, 0f) }; + elements[4] = new QuadElement() { Pos = new float2(0.5f, 0f) }; + // these two nodes push the count above the max leaf amount and thus need to be stored inside a sub node + elements[5] = new QuadElement() { Pos = new float2(3f, 0f) }; + elements[6] = new QuadElement() { Pos = new float2(3.5f, 0f) }; + + const int size = 10; + AABB2D bounds = new AABB2D(new float2(size, -1f), new float2(size, 4f)); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 4, maxLeafElements: 5); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } + + [Test] + public void MultiDepthTree2() + { + NativeArray> elements = new NativeArray>(7, Allocator.TempJob); + // Depth 2 - data should be stored in depth 2 nodes + elements[0] = new QuadElement() { Pos = new float2(0.1f, 3f) }; // Morton code 10 + elements[1] = new QuadElement() { Pos = new float2(0.2f, 3f) }; // Morton code 10 + elements[2] = new QuadElement() { Pos = new float2(5.3f, 3f) }; // Morton code 11 + elements[3] = new QuadElement() { Pos = new float2(5.4f, 3f) }; // Morton code 11 + // Depth 1 - data should be stored in depth 1 nodes + elements[4] = new QuadElement() { Pos = new float2(12.5f, 7f) }; // Morton code 12 + elements[5] = new QuadElement() { Pos = new float2(12.0f, 7f) }; // Morton code 12 + // Depth 1 - data should be stored in depth 1 nodes + elements[6] = new QuadElement() { Pos = new float2(12.5f, 2f) }; // Morton code 14 + + AABB2D bounds = new AABB2D(10f, 10f); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 2, maxLeafElements: 3); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } + + [Test] + public void MultiDepthTree3() + { + NativeArray> elements = new NativeArray>(6, Allocator.TempJob); + // Depth 2 - data should be stored in depth 2 nodes + elements[0] = new QuadElement() { Pos = new float2(0.1f, 3f) }; // Morton code 10 + elements[1] = new QuadElement() { Pos = new float2(0.2f, 3f) }; // Morton code 10 + elements[2] = new QuadElement() { Pos = new float2(5.3f, 3f) }; // Morton code 11 + elements[3] = new QuadElement() { Pos = new float2(5.4f, 3f) }; // Morton code 11 + // Depth 1 - data should be stored in depth 1 nodes + elements[4] = new QuadElement() { Pos = new float2(12.5f, 7f) }; // Morton code 12 + // Depth 1 - data should be stored in depth 1 nodes + elements[5] = new QuadElement() { Pos = new float2(2.5f, 12f) }; // Morton code 4 + + AABB2D bounds = new AABB2D(10f, 10f); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 2, maxLeafElements: 3); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } + + [Test] + public void LargeUtilisation() + { + NativeArray> elements = new NativeArray>(600, Allocator.TempJob); + for (int i = 0; i < elements.Length; i++) + { + elements[i] = new QuadElement() { Pos = new float2(0.01f * i, 3f) }; + } + + AABB2D bounds = new AABB2D(5f, 10f); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 5, maxLeafElements: 600); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } + + [Test] + public void LargeUtilisation2() + { + NativeArray> elements = new NativeArray>(1040, Allocator.TempJob); + for (int i = 0; i < elements.Length; i++) + { + elements[i] = new QuadElement() { Pos = new float2(0.01f * i, 3f) }; + } + + AABB2D bounds = new AABB2D(new float2(22f, 5f), new float2(44f, 6)); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 5, maxLeafElements: 1000); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } + + [Test] + public void LargeUtilisation3() + { + NativeArray> elements = new NativeArray>(400, Allocator.TempJob); + for (int i = 0; i < elements.Length; i++) + { + elements[i] = new QuadElement() { Pos = new float2(0.05f * i, 3f) }; + } + + AABB2D bounds = new AABB2D(new float2(22f, 5f), new float2(44f, 6)); + NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 7, maxLeafElements: 300); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.PrintDepthUtilisation(treeRef); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); } } diff --git a/Assets/Example.meta b/Assets/Example.meta new file mode 100644 index 0000000..3b9909b --- /dev/null +++ b/Assets/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 324d2b59e97211a409744276a41ffba6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Example/NativeQuadTree.Example.asmdef b/Assets/Example/NativeQuadTree.Example.asmdef new file mode 100644 index 0000000..74f15b3 --- /dev/null +++ b/Assets/Example/NativeQuadTree.Example.asmdef @@ -0,0 +1,19 @@ +{ + "name": "NativeQuadTree.Example", + "rootNamespace": "", + "references": [ + "GUID:d8b63aba1907145bea998dd612889d6b", + "GUID:4259b4454b3b86a4790bd00dd10d9797", + "GUID:8a2eafa29b15f444eb6d74f94a930e1d", + "GUID:e0cd26848372d4e5c891c569017e11f1" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Example/NativeQuadTree.Example.asmdef.meta b/Assets/Example/NativeQuadTree.Example.asmdef.meta new file mode 100644 index 0000000..2e81441 --- /dev/null +++ b/Assets/Example/NativeQuadTree.Example.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: da585fe5cee378b4aa0f48150482b557 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Example/NativeQuadTreeCreator.cs b/Assets/Example/NativeQuadTreeCreator.cs new file mode 100644 index 0000000..866ec77 --- /dev/null +++ b/Assets/Example/NativeQuadTreeCreator.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using NativeQuadTree; +using NativeQuadTree.Helpers; +using NativeQuadTree.Jobs; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEditor.Graphs; +using UnityEngine; +using Debug = UnityEngine.Debug; +using Random = UnityEngine.Random; + +[RequireComponent(typeof(RectTransform))] +public class NativeQuadTreeCreator : MonoBehaviour +{ + public NativeQuadTree tree; + [Range(1, 8)] + public int Depth = 6; + public ushort LeafUnits = 200; + + public int PositionCount = 2000; + public List> Positions = new List>(); + + private RectTransform trans; + + void Start() + { + trans = GetComponent(); + + AABB2D rect = new AABB2D(new float2(trans.position.x, trans.position.y), (trans.rect.max - trans.rect.min) / 2f); + tree = new NativeQuadTree(rect, Allocator.Persistent, Depth, LeafUnits); + + do + { + float2 testPos = new float2( + Random.Range(rect.Center.x - rect.Extents.x, rect.Center.x + rect.Extents.x), + Random.Range(rect.Center.y - rect.Extents.y, rect.Center.y + rect.Extents.y)); + + if(rect.Contains(testPos)) + { + Positions.Add(new QuadElement() + { + Pos = testPos + }); + } + } + while (Positions.Count < PositionCount); + + AddBulkJob bulkJob = new AddBulkJob(); + bulkJob.Elements = new NativeArray>(Positions.ToArray(), Allocator.TempJob); + bulkJob.QuadTree = new NativeReference>(tree, Allocator.TempJob); + + Stopwatch stopwatch = Stopwatch.StartNew(); + bulkJob.Schedule().Complete(); + stopwatch.Stop(); + Debug.Log("Bulk Add Duration: " + stopwatch.ElapsedMilliseconds + " ms"); + + ValidationHelpers.ValidateNativeTreeContent(bulkJob.QuadTree, bulkJob.Elements); + ValidationHelpers.BruteForceLocationHitCheck(bulkJob.QuadTree, bulkJob.Elements); + + bulkJob.Elements.Dispose(); + bulkJob.QuadTree.Dispose(); + } + + private void OnDrawGizmos() + { + if(trans == null) trans = GetComponent(); + Gizmos.color = Color.white; + + // draw box + DrawRectSubdivide(new AABB2D(trans), Depth); + + Gizmos.color = Color.white; + const float size = 0.2f; + foreach (QuadElement position in Positions) + { + Gizmos.DrawLine( + new Vector3(position.Pos.x - size, position.Pos.y - size), + new Vector3(position.Pos.x + size, position.Pos.y + size)); + Gizmos.DrawLine( + new Vector3(position.Pos.x - size, position.Pos.y + size), + new Vector3(position.Pos.x + size, position.Pos.y - size)); + } + } + + private void DrawRect(AABB2D rect) + { + float rectXMin = rect.Center.x - rect.Extents.x; + float rectXMax = rect.Center.x + rect.Extents.x; + float rectYMin = rect.Center.y - rect.Extents.y; + float rectYMax = rect.Center.y + rect.Extents.y; + + Gizmos.DrawLine( + new Vector3(rectXMin, rectYMin), + new Vector3(rectXMax, rectYMin)); + Gizmos.DrawLine( + new Vector3(rectXMin, rectYMax), + new Vector3(rectXMax, rectYMax)); + Gizmos.DrawLine( + new Vector3(rectXMin, rectYMin), + new Vector3(rectXMin, rectYMax)); + Gizmos.DrawLine( + new Vector3(rectXMax, rectYMin), + new Vector3(rectXMax, rectYMax)); + } + + private void DrawRectSubdivide(AABB2D rect, int division) + { + if(division > 0) + { + var half = rect.Extents / 2f; + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x - half.x, rect.Center.y + half.y), half), division - 1); + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x + half.x, rect.Center.y + half.y), half), division - 1); + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x - half.x, rect.Center.y - half.y), half), division - 1); + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x + half.x, rect.Center.y - half.y), half), division - 1); + } + + Gizmos.color = Color.Lerp(new Color(0.34f, 0.34f, 0.34f), new Color(1f, 1f, 1f), (float)division / Depth); + DrawRect(rect); + } +} diff --git a/Assets/Example/NativeQuadTreeCreator.cs.meta b/Assets/Example/NativeQuadTreeCreator.cs.meta new file mode 100644 index 0000000..31a8801 --- /dev/null +++ b/Assets/Example/NativeQuadTreeCreator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8f3a2756cc3eca64d9742d7ae481dca7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Example/NativeQueryCircle.cs b/Assets/Example/NativeQueryCircle.cs new file mode 100644 index 0000000..fbee8ac --- /dev/null +++ b/Assets/Example/NativeQueryCircle.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NativeQuadTree; +using NativeQuadTree.Helpers; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +public class NativeQueryCircle : MonoBehaviour +{ + public NativeQuadTreeCreator Tree; + public float Radious = 10; + + private Transform trans; + private QuadElement[] Results; + + void Start() + { + trans = transform; + } + + void Update() + { + Circle2D circle = new Circle2D(new float2(trans.position.x, trans.position.y), Radious); + NativeReference> treeRef = new NativeReference>(Tree.tree, Allocator.TempJob); + NativeList> results = new NativeList>(Tree.tree.EstimateResultSize(circle), Allocator.TempJob); + + CircleQueryJob query = new CircleQueryJob(circle, treeRef, results); + query.Schedule().Complete(); + + Results = results.ToArray(); + results.Dispose(); + treeRef.Dispose(); + } + + private void OnDrawGizmos() + { + if(trans == null) trans = transform; + + Gizmos.color = Color.green; + + Vector2 previousPos = trans.position + new Vector3(0f, Radious); + const int steps = 48; + const float stepDegree = (360f / steps); + for (int i = 1; i <= steps; i++) + { + Vector2 newPos = trans.position + new Vector3( + Radious * math.sin(math.radians(stepDegree * i)), + Radious * math.cos(math.radians(stepDegree * i))); + + Gizmos.DrawLine(previousPos, newPos); + previousPos = newPos; + } + + const float size = 0.2f; + foreach (QuadElement result in Results ?? Array.Empty>()) + { + Gizmos.DrawLine( + new Vector3(result.Pos.x - size, result.Pos.y), + new Vector3(result.Pos.x + size, result.Pos.y)); + Gizmos.DrawLine( + new Vector3(result.Pos.x, result.Pos.y - size), + new Vector3(result.Pos.x, result.Pos.y + size)); + } + } +} diff --git a/Assets/Example/NativeQueryCircle.cs.meta b/Assets/Example/NativeQueryCircle.cs.meta new file mode 100644 index 0000000..1231857 --- /dev/null +++ b/Assets/Example/NativeQueryCircle.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 340cce05e5e3be54ab016a2bc7ab92bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Example/NativeQueryRect.cs b/Assets/Example/NativeQueryRect.cs new file mode 100644 index 0000000..95f9e15 --- /dev/null +++ b/Assets/Example/NativeQueryRect.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NativeQuadTree; +using NativeQuadTree.Helpers; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +[RequireComponent(typeof(RectTransform))] +public class NativeQueryRect : MonoBehaviour +{ + public NativeQuadTreeCreator Tree; + + private RectTransform trans; + private QuadElement[] Results; + + void Start() + { + trans = GetComponent(); + } + + void Update() + { + AABB2D box = new AABB2D(new float2(trans.position.x, trans.position.y), (trans.rect.max - trans.rect.min) / 2f); + NativeReference> treeRef = new NativeReference>(Tree.tree, Allocator.TempJob); + NativeList> results = new NativeList>(Tree.tree.EstimateResultSize(box), Allocator.TempJob); + + RectQueryJob query = new RectQueryJob(box, treeRef, results); + query.Schedule().Complete(); + + Results = results.ToArray(); + results.Dispose(); + treeRef.Dispose(); + } + + private void OnDrawGizmos() + { + if(trans == null) trans = GetComponent(); + + Gizmos.color = new Color(1f, 0.38f, 0.12f); + + Rect transRect = trans.rect; + float transRectXMin = trans.position.x + transRect.xMin; + float transRectXMax = trans.position.x + transRect.xMax; + float transRectYMin = trans.position.y + transRect.yMin; + float transRectYMax = trans.position.y + transRect.yMax; + Gizmos.DrawLine( + new Vector3(transRectXMin, transRectYMin), + new Vector3(transRectXMax, transRectYMin)); + Gizmos.DrawLine( + new Vector3(transRectXMin, transRectYMax), + new Vector3(transRectXMax, transRectYMax)); + Gizmos.DrawLine( + new Vector3(transRectXMin, transRectYMin), + new Vector3(transRectXMin, transRectYMax)); + Gizmos.DrawLine( + new Vector3(transRectXMax, transRectYMin), + new Vector3(transRectXMax, transRectYMax)); + + const float size = 0.2f; + foreach (QuadElement result in Results ?? Array.Empty>()) + { + float xMin = result.Pos.x - size; + float xMax = result.Pos.x + size; + float yMin = result.Pos.y - size; + float yMax = result.Pos.y + size; + + Gizmos.DrawLine( + new Vector3(xMin, yMin), + new Vector3(xMax, yMin)); + Gizmos.DrawLine( + new Vector3(xMin, yMax), + new Vector3(xMax, yMax)); + Gizmos.DrawLine( + new Vector3(xMin, yMin), + new Vector3(xMin, yMax)); + Gizmos.DrawLine( + new Vector3(xMax, yMin), + new Vector3(xMax, yMax)); + } + } +} diff --git a/Assets/Example/NativeQueryRect.cs.meta b/Assets/Example/NativeQueryRect.cs.meta new file mode 100644 index 0000000..41838cb --- /dev/null +++ b/Assets/Example/NativeQueryRect.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a5c8b98c77794afdbf38dfed292230f6 +timeCreated: 1630332880 \ No newline at end of file diff --git a/Assets/Example/Test Scene.unity b/Assets/Example/Test Scene.unity new file mode 100644 index 0000000..2af8aaf --- /dev/null +++ b/Assets/Example/Test Scene.unity @@ -0,0 +1,353 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &559260069 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 559260071} + - component: {fileID: 559260070} + m_Layer: 0 + m_Name: QueryRect + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &559260070 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 559260069} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a5c8b98c77794afdbf38dfed292230f6, type: 3} + m_Name: + m_EditorClassIdentifier: + Tree: {fileID: 1933793337} +--- !u!224 &559260071 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 559260069} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -6.3946342} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 3 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -14.07, y: -6.11} + m_SizeDelta: {x: 14, y: 14} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &1112568852 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1112568855} + - component: {fileID: 1112568854} + - component: {fileID: 1112568853} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &1112568853 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1112568852} + m_Enabled: 1 +--- !u!20 &1112568854 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1112568852} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_FocalLength: 50 + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 33.449852 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &1112568855 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1112568852} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1739482046 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1739482048} + - component: {fileID: 1739482047} + m_Layer: 0 + m_Name: QueryCircle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1739482047 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1739482046} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 340cce05e5e3be54ab016a2bc7ab92bc, type: 3} + m_Name: + m_EditorClassIdentifier: + Tree: {fileID: 1933793337} + Radious: 8.71 +--- !u!4 &1739482048 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1739482046} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 11.13, y: 6.22, z: -6.3946342} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 2 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1933793336 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1933793338} + - component: {fileID: 1933793337} + m_Layer: 0 + m_Name: NativeTree + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1933793337 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1933793336} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 8f3a2756cc3eca64d9742d7ae481dca7, type: 3} + m_Name: + m_EditorClassIdentifier: + Depth: 5 + LeafUnits: 1000 + PositionCount: 2000 +--- !u!224 &1933793338 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1933793336} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 1 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 50, y: 50} + m_Pivot: {x: 0.5, y: 0.5} diff --git a/Assets/Example/Test Scene.unity.meta b/Assets/Example/Test Scene.unity.meta new file mode 100644 index 0000000..4ca4bfd --- /dev/null +++ b/Assets/Example/Test Scene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 0511f390df3de7f4f9d2c776dee8f5c0 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Helpers.meta b/Assets/Helpers.meta new file mode 100644 index 0000000..56e5826 --- /dev/null +++ b/Assets/Helpers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b2f64b6bb3155a44ab1e5d8ece68b943 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Helpers/ArraySizeHelpers.cs b/Assets/Helpers/ArraySizeHelpers.cs new file mode 100644 index 0000000..3eda6c8 --- /dev/null +++ b/Assets/Helpers/ArraySizeHelpers.cs @@ -0,0 +1,41 @@ +using Unity.Mathematics; + +namespace NativeQuadTree.Helpers +{ + public static class ArraySizeHelpers + { + /// + /// Calculates an estimate for the amount of results from an entityQuery assuming perfect uniform entry distribution inside + /// + /// NativeQuadTree that will be queried against + /// shape that will be used as range filter + /// estimated array size + public static int EstimateResultSize(this NativeQuadTree tree, Circle2D queryShape) where T : unmanaged + { + if(tree.EntryCount == 0) return 0; + + float boundsArea = tree.bounds.Size.x * tree.bounds.Size.y; + float shapeArea = (math.PI * queryShape.Radious) * (math.PI * queryShape.Radious); + + float itemsPerUnit = boundsArea / tree.EntryCount; + return (int) (shapeArea * itemsPerUnit); + } + + /// + /// Calculates an estimate for the amount of results from an entityQuery assuming perfect uniform entry distribution inside + /// + /// NativeQuadTree that will be queried against + /// shape that will be used as range filter + /// estimated array size + public static int EstimateResultSize(this NativeQuadTree tree, AABB2D queryShape) where T : unmanaged + { + if(tree.EntryCount == 0) return 0; + + float boundsArea = tree.bounds.Size.x * tree.bounds.Size.y; + float shapeArea = queryShape.Size.x * queryShape.Size.y; + + float itemsPerUnit = boundsArea / tree.EntryCount; + return (int) (shapeArea * itemsPerUnit); + } + } +} \ No newline at end of file diff --git a/Assets/Helpers/ArraySizeHelpers.cs.meta b/Assets/Helpers/ArraySizeHelpers.cs.meta new file mode 100644 index 0000000..a008caa --- /dev/null +++ b/Assets/Helpers/ArraySizeHelpers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 067951e650304ad996cdfdc6c1905729 +timeCreated: 1630346708 \ No newline at end of file diff --git a/Assets/LookupTables.cs b/Assets/Helpers/LookupTables.cs similarity index 100% rename from Assets/LookupTables.cs rename to Assets/Helpers/LookupTables.cs diff --git a/Assets/Helpers/LookupTables.cs.meta b/Assets/Helpers/LookupTables.cs.meta new file mode 100644 index 0000000..19163b0 --- /dev/null +++ b/Assets/Helpers/LookupTables.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6bb926e55c6a9d244a689e657c15a221 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Helpers/NativeQuadTreeDrawHelpers.cs b/Assets/Helpers/NativeQuadTreeDrawHelpers.cs new file mode 100644 index 0000000..a7d8597 --- /dev/null +++ b/Assets/Helpers/NativeQuadTreeDrawHelpers.cs @@ -0,0 +1,73 @@ +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; +using UnityEngine; + +namespace NativeQuadTree +{ + /// + /// Editor drawing of the NativeQuadTree + /// + public unsafe struct NativeQuadTreeDrawHelpers where T : unmanaged + { + public static void Draw(NativeQuadTree tree, NativeList> results, AABB2D range, Color[][] texture) + { + float widthMult = texture.Length / tree.bounds.Extents.x * 2 / 2 / 2; + float heightMult = texture[0].Length / tree.bounds.Extents.y * 2 / 2 / 2; + + float widthAdd = tree.bounds.Center.x + tree.bounds.Extents.x; + float heightAdd = tree.bounds.Center.y + tree.bounds.Extents.y; + + for (int i = 0; i < tree.nodes->Capacity; i++) + { + QuadNode node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, i); + + if(node.count > 0) + { + for (int k = 0; k < node.count; k++) + { + QuadElement element = + UnsafeUtility.ReadArrayElement>(tree.elements->Ptr, node.firstChildIndex + k); + + texture[(int) ((element.Pos.x + widthAdd) * widthMult)] + [(int) ((element.Pos.y + heightAdd) * heightMult)] = Color.red; + } + } + } + + foreach (QuadElement element in results) + { + texture[(int) ((element.Pos.x + widthAdd) * widthMult)] + [(int) ((element.Pos.y + heightAdd) * heightMult)] = Color.green; + } + + DrawBounds(texture, range, tree); + } + + private static void DrawBounds(Color[][] texture, AABB2D bounds, NativeQuadTree tree) + { + float widthMult = texture.Length / tree.bounds.Extents.x * 2 / 2 / 2; + float heightMult = texture[0].Length / tree.bounds.Extents.y * 2 / 2 / 2; + + float widthAdd = tree.bounds.Center.x + tree.bounds.Extents.x; + float heightAdd = tree.bounds.Center.y + tree.bounds.Extents.y; + + float2 top = new float2(bounds.Center.x, bounds.Center.y - bounds.Extents.y); + float2 left = new float2(bounds.Center.x - bounds.Extents.x, bounds.Center.y); + + for (int leftToRight = 0; leftToRight < bounds.Extents.x * 2; leftToRight++) + { + float poxX = left.x + leftToRight; + texture[(int) ((poxX + widthAdd) * widthMult)][(int) ((bounds.Center.y + heightAdd + bounds.Extents.y) * heightMult)] = Color.blue; + texture[(int) ((poxX + widthAdd) * widthMult)][(int) ((bounds.Center.y + heightAdd - bounds.Extents.y) * heightMult)] = Color.blue; + } + + for (int topToBottom = 0; topToBottom < bounds.Extents.y * 2; topToBottom++) + { + float posY = top.y + topToBottom; + texture[(int) ((bounds.Center.x + widthAdd + bounds.Extents.x) * widthMult)][(int) ((posY + heightAdd) * heightMult)] = Color.blue; + texture[(int) ((bounds.Center.x + widthAdd - bounds.Extents.x) * widthMult)][(int) ((posY + heightAdd) * heightMult)] = Color.blue; + } + } + } +} \ No newline at end of file diff --git a/Assets/Helpers/NativeQuadTreeDrawHelpers.cs.meta b/Assets/Helpers/NativeQuadTreeDrawHelpers.cs.meta new file mode 100644 index 0000000..afd4ffb --- /dev/null +++ b/Assets/Helpers/NativeQuadTreeDrawHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9977f4fb9ccb499498f21ebff5af7b05 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Helpers/ValidationHelpers.cs b/Assets/Helpers/ValidationHelpers.cs new file mode 100644 index 0000000..b2c18ed --- /dev/null +++ b/Assets/Helpers/ValidationHelpers.cs @@ -0,0 +1,195 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Assertions; +using Debug = UnityEngine.Debug; + +namespace NativeQuadTree.Helpers +{ + public static class ValidationHelpers + { + /// + /// Check that all expected counters match the expected entry count from the source data that was added + /// + /// tree to check + /// Expected entries + [Conditional("UNITY_ASSERTIONS"), BurstDiscard] + public static void ValidateNativeTreeContent(NativeReference> tree, NativeArray> entries) where T : unmanaged + { + Assert.AreEqual(entries.Length, tree.Value.EntryCount, "Tree length mismatch (Count)!"); + + unsafe + { + UnsafeList* values = tree.Value.elements; + int treeLength = values->Length; + Assert.IsTrue(entries.Length <= treeLength, "Tree length mismatch (Raw Data)!"); + } + + // validate that the node counts match the expected entity count + + int nodeCount = 0; + int leafCount = 0; + List rawNodes = ExtractNodeValues(tree.Value); + for (int i = 0; i < rawNodes.Count; i++) + { + if(rawNodes[i].isLeaf) + { + nodeCount += rawNodes[i].count; + leafCount++; + } + } + Assert.AreEqual(entries.Length, nodeCount, "Tree length mismatch (Nodes)!"); + } + + internal static List ExtractNodeValues(NativeQuadTree tree) where T : unmanaged + { + unsafe + { + // validate that the node counts match the expected entity count + List rawNodes = new List(tree.nodes->Length); + UnsafeList* nodes = tree.nodes; + for (int i = 0; i < nodes->Length; i++) + { + // this converts to the actual nodes we can inspect to make future actions easier + QuadNode node = UnsafeUtility.ReadArrayElement(nodes->Ptr, i); + rawNodes.Add(node); + } + + return rawNodes; + } + } + + /// + /// Check that all expected counters match the expected entry count from the source data that was added + /// + /// tree to check + /// Expected entries + [Conditional("UNITY_ASSERTIONS"), BurstDiscard] + public static void BruteForceLocationHitCheck(NativeReference> treeRef, NativeArray> entries) where T : unmanaged + { + BruteForceLocationHitCheckRect(treeRef, entries); + BruteForceLocationHitCheckCircle(treeRef, entries); + } + + /// + /// Check that all expected counters match the expected entry count from the source data that was added + /// + /// tree to check + /// Expected entries + [Conditional("UNITY_ASSERTIONS"), BurstDiscard] + public static void BruteForceLocationHitCheckCircle(NativeReference> treeRef, NativeArray> entries) where T : unmanaged + { + NativeQuadTree tree = treeRef.Value; + + for (int i = 0; i < entries.Length; i++) + { + QuadElement entry = entries[i]; + Circle2D exactPosition = new Circle2D(entry.Pos, 0.0001f); + + NativeList> resultArray = new NativeList>(2, Allocator.TempJob); + tree.RangeQuery(exactPosition, resultArray); + + if(resultArray.Length == 0) + { + // use explicit if statement so that you can put a breakpoint here and diagnose the issue + Assert.IsTrue(false, "no results for entry query at " + i); + } + + bool found = false; + foreach (QuadElement result in resultArray) + { + if(Equals(result.Element, entry.Element)) + { + // expected result was actually found in the data + found = true; + break; + } + } + + resultArray.Dispose(); + Assert.IsTrue(found, "Missing expected quadTree entry " + i); + } + } + + /// + /// Check that all expected counters match the expected entry count from the source data that was added + /// + /// tree to check + /// Expected entries + [Conditional("UNITY_ASSERTIONS"), BurstDiscard] + public static void BruteForceLocationHitCheckRect(NativeReference> treeRef, NativeArray> entries) where T : unmanaged + { + NativeQuadTree tree = treeRef.Value; + + for (int i = 0; i < entries.Length; i++) + { + QuadElement entry = entries[i]; + AABB2D exactPosition = new AABB2D(entry.Pos, 0.0001f); + + NativeList> resultArray = new NativeList>(2, Allocator.TempJob); + tree.RangeQuery(exactPosition, resultArray); + + if(resultArray.Length == 0) + { + // use explicit if statement so that you can put a breakpoint here and diagnose the issue + Assert.IsTrue(false, "no results for entry query at " + i); + } + + bool found = false; + foreach (QuadElement result in resultArray) + { + if(Equals(result.Element, entry.Element)) + { + // expected result was actually found in the data + found = true; + break; + } + } + + resultArray.Dispose(); + Assert.IsTrue(found, "Missing expected quadTree entry " + i); + } + } + + [BurstDiscard, Conditional("UNITY_EDITOR")] + public static void PrintDepthUtilisation(NativeReference> treeRef) where T : unmanaged + { + PrintDepthUtilisation(treeRef.Value); + } + + [BurstDiscard, Conditional("UNITY_EDITOR")] + public static void PrintDepthUtilisation(NativeQuadTree tree) where T : unmanaged + { + StringBuilder builder = new StringBuilder("Depth Utilisation - Percentage of used nodes in a depth level with the total count of " + + "Elements stored inside the the depth level").AppendLine(); + List rawNodes = ExtractNodeValues(tree); + + int previousDepthSize = LookupTables.DepthSizeLookup[1]; + for (int i = 2; i <= tree.MaxDepth + 1; i++) + { + int totalUnits = 0; + int depthNodes = LookupTables.DepthSizeLookup[i]; + int usedNodes = 0; + + for (int j = previousDepthSize; j < depthNodes; j++) + { + QuadNode quadNode = rawNodes[j]; + + totalUnits += quadNode.count; + if(quadNode.count > 0) + { + usedNodes++; + } + } + + builder.AppendLine($"Depth {i - 1}: {(usedNodes / (float)(depthNodes - previousDepthSize) * 100f):N3}% Utilised - {usedNodes} Nodes ({totalUnits} Elements)"); + previousDepthSize = depthNodes; + } + + Debug.Log(builder.ToString()); + } + } +} \ No newline at end of file diff --git a/Assets/Helpers/ValidationHelpers.cs.meta b/Assets/Helpers/ValidationHelpers.cs.meta new file mode 100644 index 0000000..15bc063 --- /dev/null +++ b/Assets/Helpers/ValidationHelpers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f4d199c3b3f4d7149ae2aafd66902306 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs.meta b/Assets/Jobs.meta new file mode 100644 index 0000000..166da76 --- /dev/null +++ b/Assets/Jobs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 28295c755ea4284448fcc53609e95b23 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/AddBulkJob.cs b/Assets/Jobs/AddBulkJob.cs new file mode 100644 index 0000000..c4e7b3a --- /dev/null +++ b/Assets/Jobs/AddBulkJob.cs @@ -0,0 +1,34 @@ +using System.Diagnostics; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; + +namespace NativeQuadTree.Jobs +{ + /// + /// Bulk insert many items into the tree + /// + [BurstCompile] + public struct AddBulkJob : IJob where T : unmanaged + { + [ReadOnly] + public NativeArray> Elements; + + public NativeReference> QuadTree; + + public void Execute() + { + NativeQuadTree quadTree = QuadTree.Value; + quadTree.ClearAndBulkInsert(Elements); + QuadTree.Value = quadTree; + + ValidateData(); + } + + [BurstDiscard, Conditional("UNITY_ASSERTIONS")] + private void ValidateData() + { + UnityEngine.Assertions.Assert.AreEqual(Elements.Length, QuadTree.Value.EntryCount, "Failed to populate entityData"); + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/AddBulkJob.cs.meta b/Assets/Jobs/AddBulkJob.cs.meta new file mode 100644 index 0000000..020b717 --- /dev/null +++ b/Assets/Jobs/AddBulkJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 115f96139f157634aadf08d16d9b66c6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/CircleQueryJob.cs b/Assets/Jobs/CircleQueryJob.cs new file mode 100644 index 0000000..1d5fc83 --- /dev/null +++ b/Assets/Jobs/CircleQueryJob.cs @@ -0,0 +1,36 @@ +using NativeQuadTree.Jobs.Internal; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; + +namespace NativeQuadTree +{ + /// + /// Example on how to do a range query, it's better to write your own and do many queries in a batch + /// + [BurstCompile] + public struct CircleQueryJob : IJob where T : unmanaged + { + [ReadOnly] + public Circle2D Bounds; + [ReadOnly] + public NativeReference> QuadTree; + public NativeList> Results; + + private QuadTreeCircleRangeQuery query; + + public CircleQueryJob(Circle2D bounds, NativeReference> quadTree, NativeList> results) + { + Bounds = bounds; + QuadTree = quadTree; + Results = results; + + query = new QuadTreeCircleRangeQuery(); + } + + public void Execute() + { + query.Query(QuadTree.Value, Bounds, Results); + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/CircleQueryJob.cs.meta b/Assets/Jobs/CircleQueryJob.cs.meta new file mode 100644 index 0000000..ba7f312 --- /dev/null +++ b/Assets/Jobs/CircleQueryJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ff19164091c1327439ce1a0ae937cd53 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/Internal.meta b/Assets/Jobs/Internal.meta new file mode 100644 index 0000000..d32f6a7 --- /dev/null +++ b/Assets/Jobs/Internal.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1285260ab7a49a248bcca4e38db02ea4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs new file mode 100644 index 0000000..dfc017c --- /dev/null +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -0,0 +1,100 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace NativeQuadTree.Jobs.Internal +{ + public unsafe struct QuadTreeCircleRangeQuery where T : unmanaged + { + private NativeQuadTree tree; + + [NativeDisableUnsafePtrRestriction] + private UnsafeList* fastResults; + private int count; + + private Circle2D bounds; + + public void Query(NativeQuadTree tree, Circle2D bounds, NativeList> results) + { + this.tree = tree; + this.bounds = bounds; + count = 0; + + // Get pointer to inner list data for faster writing + fastResults = (UnsafeList*) NativeListUnsafeUtility.GetInternalListDataPtrUnchecked(ref results); + + RecursiveRangeQuery(tree.bounds, false, 1, 1); + + fastResults->Length = count; + } + + public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int prevOffset, int depth) + { + var depthSize = LookupTables.DepthSizeLookup[tree.MaxDepth - depth + 1]; + for (int l = 0; l < 4; l++) + { + var childBounds = RangeQueryHelpers.GetChildBounds(parentBounds, l); + + var contained = parentContained; + if(!contained) + { + if(bounds.Contains(childBounds)) + { + contained = true; + } + else if(!bounds.Intersects(childBounds)) + { + continue; + } + } + + + var at = prevOffset + l * depthSize; + var elementCount = UnsafeUtility.ReadArrayElement(tree.lookup->Ptr, at); + + if(elementCount > tree.MaxLeafElements && depth < tree.MaxDepth) + { + RecursiveRangeQuery(childBounds, contained, at + 1, depth + 1); + } + else if(elementCount != 0) + { + var node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, at); + + if(contained) + { + // expand to make sure the data will fit without making the result list over-sized + int targetElementSize = count + (node.count * 4); + if(targetElementSize > fastResults->Capacity) + { + fastResults->Resize>(math.max(fastResults->Capacity * 2, targetElementSize)); + } + + void* source = (void*) ((IntPtr) tree.elements->Ptr + node.firstChildIndex * UnsafeUtility.SizeOf>()); + void* destination = (void*) ((IntPtr) fastResults->Ptr + count * UnsafeUtility.SizeOf>()); + UnsafeUtility.MemCpy(destination, source, node.count * UnsafeUtility.SizeOf>()); + count += node.count; + } + else + { + // expand to make sure the data will fit without making the result list over-sized + int targetElementSize = count + (node.count * 2); + if(targetElementSize > fastResults->Capacity) + { + fastResults->Resize>(math.max(fastResults->Capacity * 2, targetElementSize)); + } + + for (int k = 0; k < node.count; k++) + { + var element = UnsafeUtility.ReadArrayElement>(tree.elements->Ptr, node.firstChildIndex + k); + if(bounds.Contains(element.Pos)) + { + UnsafeUtility.WriteArrayElement(fastResults->Ptr, count++, element); + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs.meta b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs.meta new file mode 100644 index 0000000..697c1ce --- /dev/null +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7ab7a0df3db3adc4691537d30eb681f9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs new file mode 100644 index 0000000..5a3599d --- /dev/null +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs @@ -0,0 +1,100 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace NativeQuadTree.Jobs.Internal +{ + public unsafe struct QuadTreeRectRangeQuery where T : unmanaged + { + private NativeQuadTree tree; + + [NativeDisableUnsafePtrRestriction] + private UnsafeList* fastResults; + private int count; + + private AABB2D bounds; + + public void Query(NativeQuadTree tree, AABB2D bounds, NativeList> results) + { + this.tree = tree; + this.bounds = bounds; + count = 0; + + // Get pointer to inner list data for faster writing + fastResults = (UnsafeList*) NativeListUnsafeUtility.GetInternalListDataPtrUnchecked(ref results); + + RecursiveRangeQuery(tree.bounds, false, 1, 1); + + fastResults->Length = count; + } + + public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int prevOffset, int depth) + { + var depthSize = LookupTables.DepthSizeLookup[tree.MaxDepth - depth + 1]; + for (int l = 0; l < 4; l++) + { + var childBounds = RangeQueryHelpers.GetChildBounds(parentBounds, l); + + var contained = parentContained; + if(!contained) + { + if(bounds.Contains(childBounds)) + { + contained = true; + } + else if(!bounds.Intersects(childBounds)) + { + continue; + } + } + + + var at = prevOffset + l * depthSize; + var elementCount = UnsafeUtility.ReadArrayElement(tree.lookup->Ptr, at); + + if(elementCount > tree.MaxLeafElements && depth < tree.MaxDepth) + { + RecursiveRangeQuery(childBounds, contained, at + 1, depth + 1); + } + else if(elementCount != 0) + { + var node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, at); + + if(contained) + { + // expand to make sure the data will fit without making the result list over-sized + int targetElementSize = count + (node.count * 4); + if(targetElementSize > fastResults->Capacity) + { + fastResults->Resize>(math.max(fastResults->Capacity * 2, targetElementSize)); + } + + void* source = (void*) ((IntPtr) tree.elements->Ptr + node.firstChildIndex * UnsafeUtility.SizeOf>()); + void* destination = (void*) ((IntPtr) fastResults->Ptr + count * UnsafeUtility.SizeOf>()); + UnsafeUtility.MemCpy(destination, source, node.count * UnsafeUtility.SizeOf>()); + count += node.count; + } + else + { + // expand to make sure the data will fit without making the result list over-sized + int targetElementSize = count + (node.count * 2); + if(targetElementSize > fastResults->Capacity) + { + fastResults->Resize>(math.max(fastResults->Capacity * 2, targetElementSize)); + } + + for (int k = 0; k < node.count; k++) + { + var element = UnsafeUtility.ReadArrayElement>(tree.elements->Ptr, node.firstChildIndex + k); + if(bounds.Contains(element.Pos)) + { + UnsafeUtility.WriteArrayElement(fastResults->Ptr, count++, element); + } + } + } + } + } + } + } +} diff --git a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs.meta b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs.meta new file mode 100644 index 0000000..7a0002b --- /dev/null +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e67c58fd3f0945545894dd04b8bc5e53 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/Internal/RangeQueryHelpers.cs b/Assets/Jobs/Internal/RangeQueryHelpers.cs new file mode 100644 index 0000000..2208f64 --- /dev/null +++ b/Assets/Jobs/Internal/RangeQueryHelpers.cs @@ -0,0 +1,22 @@ +using System; +using Unity.Mathematics; + +namespace NativeQuadTree.Jobs.Internal +{ + public static class RangeQueryHelpers + { + internal static AABB2D GetChildBounds(AABB2D parentBounds, int childZIndex) + { + var half = parentBounds.Extents.x * .5f; + + switch (childZIndex) + { + case 0: return new AABB2D(new float2(parentBounds.Center.x - half, parentBounds.Center.y + half), half); + case 1: return new AABB2D(new float2(parentBounds.Center.x + half, parentBounds.Center.y + half), half); + case 2: return new AABB2D(new float2(parentBounds.Center.x - half, parentBounds.Center.y - half), half); + case 3: return new AABB2D(new float2(parentBounds.Center.x + half, parentBounds.Center.y - half), half); + default: throw new Exception(); + } + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/Internal/RangeQueryHelpers.cs.meta b/Assets/Jobs/Internal/RangeQueryHelpers.cs.meta new file mode 100644 index 0000000..d0c2390 --- /dev/null +++ b/Assets/Jobs/Internal/RangeQueryHelpers.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: bebe560217974df396fac09c2e5b977f +timeCreated: 1630346234 \ No newline at end of file diff --git a/Assets/Jobs/NativeQuadTreeParallelAdd.cs b/Assets/Jobs/NativeQuadTreeParallelAdd.cs new file mode 100644 index 0000000..5cade6e --- /dev/null +++ b/Assets/Jobs/NativeQuadTreeParallelAdd.cs @@ -0,0 +1,196 @@ +using System; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; + +namespace NativeQuadTree.Jobs +{ + public static class NativeQuadTreeParallelAdd where T : unmanaged + { + public static JobHandle SetupBulkAddJobChain(NativeReference> tree, NativeArray> quadElementArray, JobHandle dependency) + { + // this whole file essentially splits out and tries to parallelize NativeQuadTree.ClearAndBulkInsert(incomingElements)! + + BulkAddInitialiseJob init = new BulkAddInitialiseJob + { + Elements = quadElementArray, + QuadTree = tree + }; + + PrepairMortonCodesJob mortonCreate = new PrepairMortonCodesJob(quadElementArray, tree); + + IndexMortonCodesJob mortonIndex = new IndexMortonCodesJob + { + MortonCodes = mortonCreate.MortonCodes, + QuadTree = tree + }; + + RecursivePrepareLeavesJob prepairLeaves = new RecursivePrepareLeavesJob + { + QuadTree = tree + }; + + AddElementsToLeafNodesJob leafJob = new AddElementsToLeafNodesJob + { + Elements = quadElementArray, + MortonCodes = mortonCreate.MortonCodes, + QuadTree = tree + }; + + const int threadBucketSize = 100; + + JobHandle initHandle = init.Schedule(dependency); + //JobHandle morton1Handle = mortonCreate.Schedule(initHandle); + JobHandle morton1Handle = mortonCreate.ScheduleBatch(quadElementArray.Length, threadBucketSize, initHandle); + JobHandle morton2Handle = mortonIndex.Schedule(morton1Handle); + //JobHandle morton2Handle = mortonIndex.ScheduleBatch(quadElementArray.Length, threadBucketSize, morton1Handle); + JobHandle prepairLeavesHandle = prepairLeaves.Schedule(morton2Handle); + JobHandle populateHandle = leafJob.Schedule(prepairLeavesHandle); + mortonCreate.MortonCodes.Dispose(populateHandle); + + return populateHandle; + } + + [BurstCompile] + private struct BulkAddInitialiseJob : IJob + { + [ReadOnly] + public NativeArray> Elements; + public NativeReference> QuadTree; + + public void Execute() + { + NativeQuadTree quadTree = QuadTree.Value; + quadTree.InitialiseBulkInsert(Elements); + QuadTree.Value = quadTree; + } + } + + [BurstCompile] + private struct PrepairMortonCodesJob : IJob, IJobParallelForBatch + { + [ReadOnly] + public NativeArray> Elements; + [ReadOnly] + public NativeReference> QuadTree; + [WriteOnly] + public NativeArray MortonCodes; + + public PrepairMortonCodesJob(NativeArray> elements, NativeReference> quadTree) : this() + { + Elements = elements; + QuadTree = quadTree; + MortonCodes = new NativeArray(Elements.Length, Allocator.TempJob); + } + + public void Execute() + { + NativeQuadTree quadTree = QuadTree.Value; + quadTree.PrepairMortonCodesInitial(Elements, MortonCodes); + } + + public void Execute(int startIndex, int count) + { + NativeQuadTree quadTree = QuadTree.Value; + float2 depthExtentsScaling = LookupTables.DepthLookup[quadTree.MaxDepth] / quadTree.bounds.Extents; + for (int i = startIndex; i < startIndex + count; i++) + { + float2 incPos = Elements[i].Pos; + incPos -= quadTree.bounds.Center; // Offset by center + incPos.y = -incPos.y; // World -> array + float2 pos = (incPos + quadTree.bounds.Extents) * .5f; // Make positive + // Now scale into available space that belongs to the depth + pos *= depthExtentsScaling; + // And interleave the bits for the morton code + MortonCodes[i] = (LookupTables.MortonLookup[(int) pos.x] | (LookupTables.MortonLookup[(int) pos.y] << 1)); + } + } + } + + [BurstCompile] + private unsafe struct IndexMortonCodesJob : IJob//ParallelForBatch + { + public NativeArray MortonCodes; + [ReadOnly] + public NativeReference> QuadTree; + + public void Execute() + { + NativeQuadTree quadTree = QuadTree.Value; + quadTree.PrepairMortonCodesIndex(MortonCodes); + } + + public void Execute(int startIndex, int count) + { + NativeQuadTree quadTree = QuadTree.Value; + // Index total child element count per node (total, so parent's counts include those of child nodes) + for (int i = startIndex; i < startIndex + count; i++) + { + int atIndex = 0; + for (int depth = 0; depth <= quadTree.MaxDepth; depth++) + { + // Increment the node on this depth that this element is contained in + (*(int*) ((IntPtr) quadTree.lookup->Ptr + atIndex * sizeof (int)))++; + atIndex = quadTree.IncrementIndex(depth, MortonCodes, i, atIndex); + } + } + } + } + + [BurstCompile] + private struct RecursivePrepareLeavesJob : IJob + { + public NativeReference> QuadTree; + + public void Execute() + { + NativeQuadTree quadTree = QuadTree.Value; + quadTree.RecursivePrepareLeaves(1, 1); + QuadTree.Value = quadTree; + } + } + + [BurstCompile] + private struct AddElementsToLeafNodesJob : IJob + { + [ReadOnly] + public NativeArray> Elements; + [ReadOnly] + public NativeArray MortonCodes; + public NativeReference> QuadTree; + + public void Execute() + { + NativeQuadTree quadTree = QuadTree.Value; + quadTree.AddElementsToLeafNodes(MortonCodes, Elements); + QuadTree.Value = quadTree; + } + + /*public void Execute(int startIndex, int count) + { + NativeQuadTree quadTree = QuadTree.Value; + for (int i = startIndex; i < count; i++) + { + int atIndex = 0; + for (int depth = 0; depth <= quadTree.MaxDepth; depth++) + { + QuadNode node = UnsafeUtility.ReadArrayElement(quadTree.nodes->Ptr, atIndex); + if(node.isLeaf) + { + // We found a leaf, add this element to it and move to the next element + UnsafeUtility.WriteArrayElement(quadTree.elements->Ptr, node.firstChildIndex + node.count, Elements[i]); + node.count++; + UnsafeUtility.WriteArrayElement(quadTree.nodes->Ptr, atIndex, node); + break; + } + + // No leaf found, we keep going deeper until we find one + atIndex = quadTree.IncrementIndex(depth, MortonCodes, i, atIndex); + } + } + }*/ + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/NativeQuadTreeParallelAdd.cs.meta b/Assets/Jobs/NativeQuadTreeParallelAdd.cs.meta new file mode 100644 index 0000000..8f12b1d --- /dev/null +++ b/Assets/Jobs/NativeQuadTreeParallelAdd.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 99fdf95183d0d8644b8b483643f8d29d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/RangeQueryJob.cs b/Assets/Jobs/RangeQueryJob.cs new file mode 100644 index 0000000..be2bc7d --- /dev/null +++ b/Assets/Jobs/RangeQueryJob.cs @@ -0,0 +1,32 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; + +namespace NativeQuadTree.Jobs +{ + /// + /// Example on how to do a range query, it's better to write your own and do many queries in a batch + /// + [BurstCompile] + public struct RangeQueryJob : IJob where T : unmanaged + { + [ReadOnly] + public AABB2D Bounds; + + [ReadOnly] + public NativeQuadTree QuadTree; + + public NativeList> Results; + + public void Execute() + { + for (int i = 0; i < 1000; i++) + { + QuadTree.RangeQuery(Bounds, Results); + Results.Clear(); + } + + QuadTree.RangeQuery(Bounds, Results); + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/RangeQueryJob.cs.meta b/Assets/Jobs/RangeQueryJob.cs.meta new file mode 100644 index 0000000..7fb9a90 --- /dev/null +++ b/Assets/Jobs/RangeQueryJob.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa170867b0f9bc14bbe6003f13cb4e10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Jobs/RectQueryJob.cs b/Assets/Jobs/RectQueryJob.cs new file mode 100644 index 0000000..49f9878 --- /dev/null +++ b/Assets/Jobs/RectQueryJob.cs @@ -0,0 +1,36 @@ +using NativeQuadTree.Jobs.Internal; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; + +namespace NativeQuadTree +{ + /// + /// Example on how to do a range query, it's better to write your own and do many queries in a batch + /// + [BurstCompile] + public struct RectQueryJob : IJob where T : unmanaged + { + [ReadOnly] + public AABB2D Bounds; + [ReadOnly] + public NativeReference> QuadTree; + public NativeList> Results; + + private QuadTreeRectRangeQuery query; + + public RectQueryJob(AABB2D bounds, NativeReference> quadTree, NativeList> results) + { + Bounds = bounds; + QuadTree = quadTree; + Results = results; + + query = new QuadTreeRectRangeQuery(); + } + + public void Execute() + { + query.Query(QuadTree.Value, Bounds, Results); + } + } +} \ No newline at end of file diff --git a/Assets/Jobs/RectQueryJob.cs.meta b/Assets/Jobs/RectQueryJob.cs.meta new file mode 100644 index 0000000..6a9f296 --- /dev/null +++ b/Assets/Jobs/RectQueryJob.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 82447950cb49438b9761d5a096579eec +timeCreated: 1630332929 \ No newline at end of file diff --git a/Assets/LookupTables.cs.meta b/Assets/LookupTables.cs.meta deleted file mode 100644 index f908bfb..0000000 --- a/Assets/LookupTables.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 3bf2086533d64f3b93c5bf841ebc44b6 -timeCreated: 1580073533 \ No newline at end of file diff --git a/Assets/NativeQuadTree.asmdef b/Assets/NativeQuadTree.asmdef new file mode 100644 index 0000000..247e036 --- /dev/null +++ b/Assets/NativeQuadTree.asmdef @@ -0,0 +1,19 @@ +{ + "name": "NativeQuadTree", + "rootNamespace": "NativeQuadTree", + "references": [ + "GUID:2665a8d13d1b3f18800f46e256720795", + "GUID:e0cd26848372d4e5c891c569017e11f1", + "GUID:8a2eafa29b15f444eb6d74f94a930e1d", + "GUID:d8b63aba1907145bea998dd612889d6b" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/NativeQuadTree.asmdef.meta b/Assets/NativeQuadTree.asmdef.meta new file mode 100644 index 0000000..4cd1f26 --- /dev/null +++ b/Assets/NativeQuadTree.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4259b4454b3b86a4790bd00dd10d9797 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 4e6d914..2de1503 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -1,27 +1,16 @@ using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Mono.Cecil.Cil; +using NativeQuadTree.Jobs.Internal; +using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; +using UnityEngine.Assertions; namespace NativeQuadTree { - // Represents an element node in the quadtree. - public struct QuadElement where T : unmanaged - { - public float2 pos; - public T element; - } - - struct QuadNode - { - // Points to this node's first child index in elements - public int firstChildIndex; - - // Number of elements in the leaf - public short count; - public bool isLeaf; - } - /// /// A QuadTree aimed to be used with Burst, supports fast bulk insertion and querying. /// @@ -29,9 +18,9 @@ struct QuadNode /// - Better test coverage /// - Automated depth / bounds / max leaf elements calculation /// - public unsafe partial struct NativeQuadTree : IDisposable where T : unmanaged + public unsafe struct NativeQuadTree : IDisposable where T : unmanaged { -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE // Safety AtomicSafetyHandle safetyHandle; [NativeSetClassTypeToNullOnSchedule] @@ -39,32 +28,35 @@ public unsafe partial struct NativeQuadTree : IDisposable where T : unmanaged #endif // Data [NativeDisableUnsafePtrRestriction] - UnsafeList* elements; + public UnsafeList* elements; [NativeDisableUnsafePtrRestriction] - UnsafeList* lookup; + public UnsafeList* lookup; [NativeDisableUnsafePtrRestriction] - UnsafeList* nodes; + public UnsafeList* nodes; - int elementsCount; + public int EntryCount => elementsCount; + private int elementsCount; - int maxDepth; - short maxLeafElements; + public int MaxDepth => m_maxDepth; + private int m_maxDepth; + public ushort MaxLeafElements => maxLeafElements; + private ushort maxLeafElements; - AABB2D bounds; // NOTE: Currently assuming uniform + internal AABB2D bounds; // NOTE: Currently assuming uniform /// /// Create a new QuadTree. /// - Ensure the bounds are not way bigger than needed, otherwise the buckets are very off. Probably best to calculate bounds /// - The higher the depth, the larger the overhead, it especially goes up at a depth of 7/8 /// - public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int maxDepth = 6, short maxLeafElements = 16, + public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int maxDepth = 6, ushort maxLeafElements = 16, int initialElementsCapacity = 256 ) : this() { this.bounds = bounds; - this.maxDepth = maxDepth; + m_maxDepth = maxDepth; this.maxLeafElements = maxLeafElements; elementsCount = 0; @@ -74,25 +66,34 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m throw new InvalidOperationException(); } -#if ENABLE_UNITY_COLLECTIONS_CHECKS - CollectionHelper.CheckIsUnmanaged(); +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE + //CollectionHelper.CheckIsUnmanaged(); DisposeSentinel.Create(out safetyHandle, out disposeSentinel, 1, allocator); #endif +#if UNITY_ASSERTIONS + // make sure that bounds are valid + Assert.IsFalse(bounds.Extents.x == 0, "bounds can't be empty! X axis must be greater than 0"); + Assert.IsFalse(bounds.Extents.y == 0, "bounds can't be empty! Y axis must be greater than 0"); + Assert.IsFalse(float.IsInfinity(bounds.Extents.x), "bounds can't be infinite! X axis is infinity"); + Assert.IsFalse(float.IsInfinity(bounds.Extents.y), "bounds can't be infinite! Y axis is infinity"); +#endif // Allocate memory for every depth, the nodes on all depths are stored in a single continuous array - var totalSize = LookupTables.DepthSizeLookup[maxDepth+1]; + int totalSize = LookupTables.DepthSizeLookup[maxDepth+1]; lookup = UnsafeList.Create(UnsafeUtility.SizeOf(), UnsafeUtility.AlignOf(), totalSize, allocator, NativeArrayOptions.ClearMemory); + lookup->Length = totalSize; nodes = UnsafeList.Create(UnsafeUtility.SizeOf(), UnsafeUtility.AlignOf(), totalSize, allocator, NativeArrayOptions.ClearMemory); + nodes->Length = totalSize; elements = UnsafeList.Create(UnsafeUtility.SizeOf>(), UnsafeUtility.AlignOf>(), @@ -101,12 +102,28 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m } public void ClearAndBulkInsert(NativeArray> incomingElements) + { + InitialiseBulkInsert(incomingElements); + + // Prepare morton codes + NativeArray mortonCodes = PrepairMortonCodes(incomingElements); + + // Prepare the tree leaf nodes + RecursivePrepareLeaves(1, 1); + + // Add elements to leaf nodes + AddElementsToLeafNodes(mortonCodes, incomingElements); + + mortonCodes.Dispose(); + } + + internal void InitialiseBulkInsert(NativeArray> incomingElements) { // Always have to clear before bulk insert as otherwise the lookup and node allocations need to account // for existing data. Clear(); -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif @@ -115,64 +132,109 @@ public void ClearAndBulkInsert(NativeArray> incomingElements) { elements->Resize>(math.max(incomingElements.Length, elements->Capacity*2)); } + + // this is needed so that future resize/move operations correctly copy the expected amount of data to the new location + elements->Length = elements->Capacity; + } - // Prepare morton codes - var mortonCodes = new NativeArray(incomingElements.Length, Allocator.Temp); - var depthExtentsScaling = LookupTables.DepthLookup[maxDepth] / bounds.Extents; - for (var i = 0; i < incomingElements.Length; i++) + [BurstCompatible] + internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray> incomingElements) + { + for (int i = 0; i < incomingElements.Length; i++) + { + int atIndex = 0; + for (int depth = 0; depth <= m_maxDepth; depth++) + { + QuadNode node = UnsafeUtility.ReadArrayElement(nodes->Ptr, atIndex); + if(node.isLeaf) + { + // We found a leaf, add this element to it and move to the next element + UnsafeUtility.WriteArrayElement(elements->Ptr, node.firstChildIndex + node.count, incomingElements[i]); + node.count++; + UnsafeUtility.WriteArrayElement(nodes->Ptr, atIndex, node); + +#if UNITY_ASSERTIONS + AssertLeafCapacityExceed(atIndex, node.count); +#endif + break; + } + // No leaf found, we keep going deeper until we find one + atIndex = IncrementIndex(depth, mortonCodes, i, atIndex); + } + +#if UNITY_ASSERTIONS + AssertLeafCapacityExceed(i, atIndex); +#endif + } + } + + [BurstDiscard, StringFormatMethod("message")] + private void AssertLeafCapacityExceed(int elementIndex, int nextNodeIndex) + { + Assert.IsFalse(elementIndex > nodes->Length, $"Failed to add element[{elementIndex}] to quad tree data!"); + } + + [BurstDiscard, StringFormatMethod("message")] + private void AssertCorrectAdd(int index, int nodeCount) + { + if(nodeCount > maxLeafElements) { - var incPos = incomingElements[i].pos; + // the allocation done in the constructor limits the amount of elements in each leaf + Assert.IsTrue(false, "Quad Tree node " + index + " is filled with elements, consider allocating a larger leaf node size than " + maxLeafElements); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private NativeArray PrepairMortonCodes(NativeArray> incomingElements) + { + NativeArray mortonCodes = new NativeArray(incomingElements.Length, Allocator.Temp); + PrepairMortonCodesInitial(incomingElements, mortonCodes); + PrepairMortonCodesIndex(mortonCodes); + return mortonCodes; + } + + /// + /// Calculates the morton code for each item inside + /// + /// all items being stored + /// temporary index for storing morton code indexes + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void PrepairMortonCodesInitial(NativeArray> incomingElements, NativeArray mortonCodes) + { + float2 depthExtentsScaling = LookupTables.DepthLookup[m_maxDepth] / bounds.Extents; + for (int i = 0; i < incomingElements.Length; i++) + { + float2 incPos = incomingElements[i].Pos; incPos -= bounds.Center; // Offset by center incPos.y = -incPos.y; // World -> array - var pos = (incPos + bounds.Extents) * .5f; // Make positive + float2 pos = (incPos + bounds.Extents) * .5f; // Make positive // Now scale into available space that belongs to the depth pos *= depthExtentsScaling; // And interleave the bits for the morton code mortonCodes[i] = (LookupTables.MortonLookup[(int) pos.x] | (LookupTables.MortonLookup[(int) pos.y] << 1)); } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void PrepairMortonCodesIndex(NativeArray mortonCodes) + { // Index total child element count per node (total, so parent's counts include those of child nodes) - for (var i = 0; i < mortonCodes.Length; i++) + for (int i = 0; i < mortonCodes.Length; i++) { int atIndex = 0; - for (int depth = 0; depth <= maxDepth; depth++) + for (int depth = 0; depth <= m_maxDepth; depth++) { // Increment the node on this depth that this element is contained in - (*(int*) ((IntPtr) lookup->Ptr + atIndex * sizeof (int)))++; + (*(int*) ((IntPtr) lookup->Ptr + (atIndex * sizeof (int))))++; atIndex = IncrementIndex(depth, mortonCodes, i, atIndex); } } - - // Prepare the tree leaf nodes - RecursivePrepareLeaves(1, 1); - - // Add elements to leaf nodes - for (var i = 0; i < incomingElements.Length; i++) - { - int atIndex = 0; - - for (int depth = 0; depth <= maxDepth; depth++) - { - var node = UnsafeUtility.ReadArrayElement(nodes->Ptr, atIndex); - if(node.isLeaf) - { - // We found a leaf, add this element to it and move to the next element - UnsafeUtility.WriteArrayElement(elements->Ptr, node.firstChildIndex + node.count, incomingElements[i]); - node.count++; - UnsafeUtility.WriteArrayElement(nodes->Ptr, atIndex, node); - break; - } - // No leaf found, we keep going deeper until we find one - atIndex = IncrementIndex(depth, mortonCodes, i, atIndex); - } - } - - mortonCodes.Dispose(); } - int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) + [BurstCompatible] + internal int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) { - var atDepth = math.max(0, maxDepth - depth); + int atDepth = math.max(0, m_maxDepth - depth); // Shift to the right and only get the first two bits int shiftedMortonCode = (mortonCodes[i] >> ((atDepth - 1) * 2)) & 0b11; // so the index becomes that... (0,1,2,3) @@ -181,15 +243,25 @@ int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) return atIndex; } - void RecursivePrepareLeaves(int prevOffset, int depth) + internal void RecursivePrepareLeaves(int prevOffset, int depth) { +#if UNITY_ASSERTIONS + if(depth == 1) + { + int[] data = new int[lookup->Length]; + for (int i = 0; i < data.Length; i++) + { + data[i] = UnsafeUtility.ReadArrayElement(lookup->Ptr, i); + } + } +#endif + for (int l = 0; l < 4; l++) { - var at = prevOffset + l * LookupTables.DepthSizeLookup[maxDepth - depth+1]; - - var elementCount = UnsafeUtility.ReadArrayElement(lookup->Ptr, at); + int at = prevOffset + l * LookupTables.DepthSizeLookup[m_maxDepth - depth+1]; + int elementCount = UnsafeUtility.ReadArrayElement(lookup->Ptr, at); - if(elementCount > maxLeafElements && depth < maxDepth) + if(elementCount > maxLeafElements && depth < m_maxDepth) { // There's more elements than allowed on this node so keep going deeper RecursivePrepareLeaves(at+1, depth+1); @@ -197,7 +269,7 @@ void RecursivePrepareLeaves(int prevOffset, int depth) else if(elementCount != 0) { // We either hit max depth or there's less than the max elements on this node, make it a leaf - var node = new QuadNode {firstChildIndex = elementsCount, count = 0, isLeaf = true }; + QuadNode node = new QuadNode {firstChildIndex = elementsCount, count = 0, isLeaf = true }; UnsafeUtility.WriteArrayElement(nodes->Ptr, at, node); elementsCount += elementCount; } @@ -206,20 +278,31 @@ void RecursivePrepareLeaves(int prevOffset, int depth) public void RangeQuery(AABB2D bounds, NativeList> results) { -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE + AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); +#endif + new QuadTreeRectRangeQuery().Query(this, bounds, results); + } + + public void RangeQuery(Circle2D bounds, NativeList> results) + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); #endif - new QuadTreeRangeQuery().Query(this, bounds, results); + new QuadTreeCircleRangeQuery().Query(this, bounds, results); } public void Clear() { -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif + UnsafeUtility.MemClear(lookup->Ptr, lookup->Capacity * UnsafeUtility.SizeOf()); UnsafeUtility.MemClear(nodes->Ptr, nodes->Capacity * UnsafeUtility.SizeOf()); UnsafeUtility.MemClear(elements->Ptr, elements->Capacity * UnsafeUtility.SizeOf>()); + elements->Clear(); + elementsCount = 0; } @@ -231,7 +314,7 @@ public void Dispose() lookup = null; UnsafeList.Destroy(nodes); nodes = null; -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE DisposeSentinel.Dispose(ref safetyHandle, ref disposeSentinel); #endif } diff --git a/Assets/NativeQuadTreeDrawing.cs b/Assets/NativeQuadTreeDrawing.cs deleted file mode 100644 index 5f056d2..0000000 --- a/Assets/NativeQuadTreeDrawing.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using Unity.Mathematics; -using UnityEngine; - -namespace NativeQuadTree -{ - /// - /// Editor drawing of the NativeQuadTree - /// - public unsafe partial struct NativeQuadTree where T : unmanaged - { - public static void Draw(NativeQuadTree tree, NativeList> results, AABB2D range, - Color[][] texture) - { - var widthMult = texture.Length / tree.bounds.Extents.x * 2 / 2 / 2; - var heightMult = texture[0].Length / tree.bounds.Extents.y * 2 / 2 / 2; - - var widthAdd = tree.bounds.Center.x + tree.bounds.Extents.x; - var heightAdd = tree.bounds.Center.y + tree.bounds.Extents.y; - - for (int i = 0; i < tree.nodes->Capacity; i++) - { - var node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, i); - - if(node.count > 0) - { - for (int k = 0; k < node.count; k++) - { - var element = - UnsafeUtility.ReadArrayElement>(tree.elements->Ptr, node.firstChildIndex + k); - - texture[(int) ((element.pos.x + widthAdd) * widthMult)] - [(int) ((element.pos.y + heightAdd) * heightMult)] = Color.red; - } - } - } - - foreach (var element in results) - { - texture[(int) ((element.pos.x + widthAdd) * widthMult)] - [(int) ((element.pos.y + heightAdd) * heightMult)] = Color.green; - } - - DrawBounds(texture, range, tree); - } - - static void DrawBounds(Color[][] texture, AABB2D bounds, NativeQuadTree tree) - { - var widthMult = texture.Length / tree.bounds.Extents.x * 2 / 2 / 2; - var heightMult = texture[0].Length / tree.bounds.Extents.y * 2 / 2 / 2; - - var widthAdd = tree.bounds.Center.x + tree.bounds.Extents.x; - var heightAdd = tree.bounds.Center.y + tree.bounds.Extents.y; - - var top = new float2(bounds.Center.x, bounds.Center.y - bounds.Extents.y); - var left = new float2(bounds.Center.x - bounds.Extents.x, bounds.Center.y); - - for (int leftToRight = 0; leftToRight < bounds.Extents.x * 2; leftToRight++) - { - var poxX = left.x + leftToRight; - texture[(int) ((poxX + widthAdd) * widthMult)][(int) ((bounds.Center.y + heightAdd + bounds.Extents.y) * heightMult)] = Color.blue; - texture[(int) ((poxX + widthAdd) * widthMult)][(int) ((bounds.Center.y + heightAdd - bounds.Extents.y) * heightMult)] = Color.blue; - } - - for (int topToBottom = 0; topToBottom < bounds.Extents.y * 2; topToBottom++) - { - var posY = top.y + topToBottom; - texture[(int) ((bounds.Center.x + widthAdd + bounds.Extents.x) * widthMult)][(int) ((posY + heightAdd) * heightMult)] = Color.blue; - texture[(int) ((bounds.Center.x + widthAdd - bounds.Extents.x) * widthMult)][(int) ((posY + heightAdd) * heightMult)] = Color.blue; - } - } - } -} \ No newline at end of file diff --git a/Assets/NativeQuadTreeDrawing.cs.meta b/Assets/NativeQuadTreeDrawing.cs.meta deleted file mode 100644 index e1e39c2..0000000 --- a/Assets/NativeQuadTreeDrawing.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: e98d676f818a45d9851041ca067a6eab -timeCreated: 1580073676 \ No newline at end of file diff --git a/Assets/NativeQuadTreeRangeQuery.cs b/Assets/NativeQuadTreeRangeQuery.cs deleted file mode 100644 index a460c9a..0000000 --- a/Assets/NativeQuadTreeRangeQuery.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using Unity.Collections; -using Unity.Collections.LowLevel.Unsafe; -using Unity.Mathematics; - -namespace NativeQuadTree -{ - public unsafe partial struct NativeQuadTree where T : unmanaged - { - struct QuadTreeRangeQuery - { - NativeQuadTree tree; - - UnsafeList* fastResults; - int count; - - AABB2D bounds; - - public void Query(NativeQuadTree tree, AABB2D bounds, NativeList> results) - { - this.tree = tree; - this.bounds = bounds; - count = 0; - - // Get pointer to inner list data for faster writing - fastResults = (UnsafeList*) NativeListUnsafeUtility.GetInternalListDataPtrUnchecked(ref results); - - RecursiveRangeQuery(tree.bounds, false, 1, 1); - - fastResults->Length = count; - } - - public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int prevOffset, int depth) - { - if(count + 4 * tree.maxLeafElements > fastResults->Capacity) - { - fastResults->Resize>(math.max(fastResults->Capacity * 2, count + 4 * tree.maxLeafElements)); - } - - var depthSize = LookupTables.DepthSizeLookup[tree.maxDepth - depth+1]; - for (int l = 0; l < 4; l++) - { - var childBounds = GetChildBounds(parentBounds, l); - - var contained = parentContained; - if(!contained) - { - if(bounds.Contains(childBounds)) - { - contained = true; - } - else if(!bounds.Intersects(childBounds)) - { - continue; - } - } - - - var at = prevOffset + l * depthSize; - - var elementCount = UnsafeUtility.ReadArrayElement(tree.lookup->Ptr, at); - - if(elementCount > tree.maxLeafElements && depth < tree.maxDepth) - { - RecursiveRangeQuery(childBounds, contained, at+1, depth+1); - } - else if(elementCount != 0) - { - var node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, at); - - if(contained) - { - var index = (void*) ((IntPtr) tree.elements->Ptr + node.firstChildIndex * UnsafeUtility.SizeOf>()); - - UnsafeUtility.MemCpy((void*) ((IntPtr) fastResults->Ptr + count * UnsafeUtility.SizeOf>()), - index, node.count * UnsafeUtility.SizeOf>()); - count += node.count; - } - else - { - for (int k = 0; k < node.count; k++) - { - var element = UnsafeUtility.ReadArrayElement>(tree.elements->Ptr, node.firstChildIndex + k); - if(bounds.Contains(element.pos)) - { - UnsafeUtility.WriteArrayElement(fastResults->Ptr, count++, element); - } - } - } - } - } - } - - static AABB2D GetChildBounds(AABB2D parentBounds, int childZIndex) - { - var half = parentBounds.Extents.x * .5f; - - switch (childZIndex) - { - case 0: return new AABB2D(new float2(parentBounds.Center.x - half, parentBounds.Center.y + half), half); - case 1: return new AABB2D(new float2(parentBounds.Center.x + half, parentBounds.Center.y + half), half); - case 2: return new AABB2D(new float2(parentBounds.Center.x - half, parentBounds.Center.y - half), half); - case 3: return new AABB2D(new float2(parentBounds.Center.x + half, parentBounds.Center.y - half), half); - default: throw new Exception(); - } - } - } - - } -} \ No newline at end of file diff --git a/Assets/QuadElement.cs b/Assets/QuadElement.cs new file mode 100644 index 0000000..d36745d --- /dev/null +++ b/Assets/QuadElement.cs @@ -0,0 +1,13 @@ +using Unity.Mathematics; + +namespace NativeQuadTree +{ + /// + /// Represents an element node in the quadtree + /// + public struct QuadElement where T : unmanaged + { + public float2 Pos; + public T Element; + } +} \ No newline at end of file diff --git a/Assets/QuadElement.cs.meta b/Assets/QuadElement.cs.meta new file mode 100644 index 0000000..1d389cf --- /dev/null +++ b/Assets/QuadElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 52de3919b8054beebd4df66840385846 +timeCreated: 1629663107 \ No newline at end of file diff --git a/Assets/QuadNode.cs b/Assets/QuadNode.cs new file mode 100644 index 0000000..94fc281 --- /dev/null +++ b/Assets/QuadNode.cs @@ -0,0 +1,15 @@ +using System.Diagnostics; + +namespace NativeQuadTree +{ + [DebuggerDisplay("QuadNode Count: {" + nameof(count) + "}{" + nameof(isLeaf) + " ? \", Leaf\" : \"\", nq}")] + internal struct QuadNode + { + // Points to this node's first child index in elements + public int firstChildIndex; + + // Number of elements in the leaf + public ushort count; + public bool isLeaf; + } +} \ No newline at end of file diff --git a/Assets/QuadNode.cs.meta b/Assets/QuadNode.cs.meta new file mode 100644 index 0000000..5da268e --- /dev/null +++ b/Assets/QuadNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4881e4f571a54c80aff08e1366452e84 +timeCreated: 1629663125 \ No newline at end of file diff --git a/Assets/QuadTreeJobs.cs b/Assets/QuadTreeJobs.cs deleted file mode 100644 index 0c303e2..0000000 --- a/Assets/QuadTreeJobs.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Unity.Burst; -using Unity.Collections; -using Unity.Jobs; - -namespace NativeQuadTree -{ - /// - /// Examples on jobs for the NativeQuadTree - /// - public static class QuadTreeJobs - { - /// - /// Bulk insert many items into the tree - /// - [BurstCompile] - public struct AddBulkJob : IJob where T : unmanaged - { - [ReadOnly] - public NativeArray> Elements; - - public NativeQuadTree QuadTree; - - public void Execute() - { - QuadTree.ClearAndBulkInsert(Elements); - } - } - - /// - /// Example on how to do a range query, it's better to write your own and do many queries in a batch - /// - [BurstCompile] - public struct RangeQueryJob : IJob where T : unmanaged - { - [ReadOnly] - public AABB2D Bounds; - - [ReadOnly] - public NativeQuadTree QuadTree; - - public NativeList> Results; - - public void Execute() - { - for (int i = 0; i < 1000; i++) - { - QuadTree.RangeQuery(Bounds, Results); - Results.Clear(); - } - QuadTree.RangeQuery(Bounds, Results); - } - } - } -} \ No newline at end of file diff --git a/Assets/QuadTreeJobs.cs.meta b/Assets/QuadTreeJobs.cs.meta deleted file mode 100644 index d29f69b..0000000 --- a/Assets/QuadTreeJobs.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 2815368828aa4ed8934169baddedefd2 -timeCreated: 1580072573 \ No newline at end of file diff --git a/Assets/package.json b/Assets/package.json new file mode 100644 index 0000000..28dae0d --- /dev/null +++ b/Assets/package.json @@ -0,0 +1,20 @@ +{ + "name": "com.crener.native_quad_tree", + "version": "0.1.0", + "displayName": "Native Quad Tree", + "description": "A powerful Unity ECS system to render massive numbers of animated sprites.", + "unity": "2020.2", + "dependencies": { + "com.unity.collections": "0.14.0-preview.16", + "com.unity.burst": "1.5.4", + "com.unity.mathematics": "1.2.1" + }, + "keywords": [ + "Dots", + "2D" + ], + "author": { + "name": "", + "url": "" + } +} diff --git a/Assets/package.json.meta b/Assets/package.json.meta new file mode 100644 index 0000000..5373701 --- /dev/null +++ b/Assets/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1ff3665ecdda995458d0f1a9d7bce4b8 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Demo Scene.PNG b/Demo Scene.PNG new file mode 100644 index 0000000..a0eecbd Binary files /dev/null and b/Demo Scene.PNG differ diff --git a/Packages/manifest.json b/Packages/manifest.json index 6c30fad..2be1b8e 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -2,16 +2,19 @@ "dependencies": { "com.unity.2d.sprite": "1.0.0", "com.unity.2d.tilemap": "1.0.0", - "com.unity.collab-proxy": "1.2.16", - "com.unity.ext.nunit": "1.0.0", - "com.unity.ide.rider": "1.1.0", - "com.unity.ide.vscode": "1.1.2", - "com.unity.physics": "0.2.4-preview", - "com.unity.test-framework": "1.1.3", - "com.unity.textmeshpro": "2.0.1", - "com.unity.timeline": "1.2.6", + "com.unity.collab-proxy": "1.5.7", + "com.unity.dots.editor": "0.12.0-preview.6", + "com.unity.entities": "0.17.0-preview.42", + "com.unity.ext.nunit": "1.0.6", + "com.unity.ide.rider": "3.0.7", + "com.unity.ide.visualstudio": "2.0.9", + "com.unity.ide.vscode": "1.2.3", + "com.unity.test-framework": "1.1.29", + "com.unity.textmeshpro": "3.0.6", + "com.unity.timeline": "1.5.6", + "com.unity.toolchain.win-x86_64-linux-x86_64": "0.1.21-preview", "com.unity.ugui": "1.0.0", - "com.unity.xr.management": "3.0.3", + "com.unity.xr.management": "4.0.5", "com.unity.modules.ai": "1.0.0", "com.unity.modules.androidjni": "1.0.0", "com.unity.modules.animation": "1.0.0", diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json new file mode 100644 index 0000000..33d0b45 --- /dev/null +++ b/Packages/packages-lock.json @@ -0,0 +1,566 @@ +{ + "dependencies": { + "com.unity.2d.sprite": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.2d.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.burst": { + "version": "1.5.4", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.mathematics": "1.2.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collab-proxy": { + "version": "1.5.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.nuget.newtonsoft-json": "2.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.collections": { + "version": "0.15.0-preview.21", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.test-framework.performance": "2.3.1-preview", + "com.unity.burst": "1.4.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.dots.editor": { + "version": "0.12.0-preview.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.entities": "0.17.0-preview.41", + "com.unity.properties": "1.6.0-preview", + "com.unity.serialization": "1.6.1-preview", + "com.unity.properties.ui": "1.6.1-preview", + "com.unity.jobs": "0.8.0-preview.23", + "com.unity.burst": "1.4.1", + "com.unity.test-framework.performance": "2.3.1-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.entities": { + "version": "0.17.0-preview.42", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.burst": "1.4.1", + "com.unity.properties": "1.5.0-preview", + "com.unity.serialization": "1.5.0-preview", + "com.unity.collections": "0.15.0-preview.21", + "com.unity.mathematics": "1.2.1", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.test-framework.performance": "2.3.1-preview", + "com.unity.nuget.mono-cecil": "0.1.6-preview.2", + "com.unity.jobs": "0.8.0-preview.23", + "com.unity.scriptablebuildpipeline": "1.9.0", + "com.unity.platforms": "0.10.0-preview.10" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ext.nunit": { + "version": "1.0.6", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.ide.rider": { + "version": "3.0.7", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.visualstudio": { + "version": "2.0.9", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.9" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ide.vscode": { + "version": "1.2.3", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.jobs": { + "version": "0.8.0-preview.23", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.collections": "0.15.0-preview.21", + "com.unity.mathematics": "1.2.1" + }, + "url": "https://packages.unity.com" + }, + "com.unity.mathematics": { + "version": "1.2.1", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.nuget.mono-cecil": { + "version": "0.1.6-preview.2", + "depth": 1, + "source": "registry", + "dependencies": { + "nuget.mono-cecil": "0.1.6-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.nuget.newtonsoft-json": { + "version": "2.0.0", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.platforms": { + "version": "0.10.0-preview.10", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.properties": "1.6.0-preview", + "com.unity.properties.ui": "1.6.2-preview.1", + "com.unity.scriptablebuildpipeline": "1.6.4-preview", + "com.unity.serialization": "1.6.2-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.properties": { + "version": "1.6.0-preview", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.nuget.mono-cecil": "0.1.6-preview.2", + "com.unity.test-framework.performance": "2.3.1-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.properties.ui": { + "version": "1.6.2-preview.1", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.properties": "1.6.0-preview", + "com.unity.serialization": "1.6.1-preview", + "com.unity.modules.uielements": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.scriptablebuildpipeline": { + "version": "1.15.2", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.serialization": { + "version": "1.6.2-preview", + "depth": 2, + "source": "registry", + "dependencies": { + "com.unity.collections": "0.12.0-preview.13", + "com.unity.burst": "1.3.5", + "com.unity.jobs": "0.5.0-preview.14", + "com.unity.properties": "1.6.0-preview", + "com.unity.test-framework.performance": "2.3.1-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.subsystemregistration": { + "version": "1.1.0", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.modules.subsystems": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.sysroot": { + "version": "0.1.19-preview", + "depth": 1, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.sysroot.linux-x86_64": { + "version": "0.1.14-preview", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.sysroot": "0.1.18-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework": { + "version": "1.1.29", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ext.nunit": "1.0.6", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.test-framework.performance": { + "version": "2.3.1-preview", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.test-framework": "1.1.0", + "com.unity.nuget.newtonsoft-json": "2.0.0-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.textmeshpro": { + "version": "3.0.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.ugui": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.timeline": { + "version": "1.5.6", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.director": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.particlesystem": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.toolchain.win-x86_64-linux-x86_64": { + "version": "0.1.21-preview", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.sysroot": "0.1.19-preview", + "com.unity.sysroot.linux-x86_64": "0.1.14-preview" + }, + "url": "https://packages.unity.com" + }, + "com.unity.ugui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0" + } + }, + "com.unity.xr.legacyinputhelpers": { + "version": "2.1.7", + "depth": 1, + "source": "registry", + "dependencies": { + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.xr": "1.0.0" + }, + "url": "https://packages.unity.com" + }, + "com.unity.xr.management": { + "version": "4.0.5", + "depth": 0, + "source": "registry", + "dependencies": { + "com.unity.modules.subsystems": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.xr": "1.0.0", + "com.unity.xr.legacyinputhelpers": "2.1.7", + "com.unity.subsystemregistration": "1.0.6" + }, + "url": "https://packages.unity.com" + }, + "nuget.mono-cecil": { + "version": "0.1.6-preview", + "depth": 2, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, + "com.unity.modules.ai": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.androidjni": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.animation": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.assetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.audio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.cloth": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.director": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.animation": "1.0.0" + } + }, + "com.unity.modules.imageconversion": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.imgui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.jsonserialize": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.particlesystem": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.physics2d": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.screencapture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.subsystems": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.terrain": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.terrainphysics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.terrain": "1.0.0" + } + }, + "com.unity.modules.tilemap": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics2d": "1.0.0" + } + }, + "com.unity.modules.ui": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.uielements": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.uielementsnative": "1.0.0" + } + }, + "com.unity.modules.uielementsnative": { + "version": "1.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.umbra": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unityanalytics": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0" + } + }, + "com.unity.modules.unitywebrequest": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.unitywebrequestassetbundle": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestaudio": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.audio": "1.0.0" + } + }, + "com.unity.modules.unitywebrequesttexture": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.unitywebrequestwww": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestassetbundle": "1.0.0", + "com.unity.modules.unitywebrequestaudio": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.imageconversion": "1.0.0" + } + }, + "com.unity.modules.vehicles": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0" + } + }, + "com.unity.modules.video": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0" + } + }, + "com.unity.modules.vr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } + }, + "com.unity.modules.wind": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": {} + }, + "com.unity.modules.xr": { + "version": "1.0.0", + "depth": 0, + "source": "builtin", + "dependencies": { + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.subsystems": "1.0.0" + } + } + } +} diff --git a/ProjectSettings/BurstAotSettings_Android.json b/ProjectSettings/BurstAotSettings_Android.json new file mode 100644 index 0000000..751473f --- /dev/null +++ b/ProjectSettings/BurstAotSettings_Android.json @@ -0,0 +1,13 @@ +{ + "MonoBehaviour": { + "Version": 3, + "EnableBurstCompilation": true, + "EnableOptimisations": true, + "EnableSafetyChecks": false, + "EnableDebugInAllBuilds": false, + "CpuMinTargetX32": 0, + "CpuMaxTargetX32": 0, + "CpuMinTargetX64": 0, + "CpuMaxTargetX64": 0 + } +} diff --git a/ProjectSettings/BurstAotSettings_StandaloneWindows.json b/ProjectSettings/BurstAotSettings_StandaloneWindows.json new file mode 100644 index 0000000..2144f6d --- /dev/null +++ b/ProjectSettings/BurstAotSettings_StandaloneWindows.json @@ -0,0 +1,16 @@ +{ + "MonoBehaviour": { + "Version": 3, + "EnableBurstCompilation": true, + "EnableOptimisations": true, + "EnableSafetyChecks": false, + "EnableDebugInAllBuilds": false, + "UsePlatformSDKLinker": false, + "CpuMinTargetX32": 0, + "CpuMaxTargetX32": 0, + "CpuMinTargetX64": 0, + "CpuMaxTargetX64": 0, + "CpuTargetsX32": 6, + "CpuTargetsX64": 72 + } +} diff --git a/ProjectSettings/CommonBurstAotSettings.json b/ProjectSettings/CommonBurstAotSettings.json new file mode 100644 index 0000000..3dffdba --- /dev/null +++ b/ProjectSettings/CommonBurstAotSettings.json @@ -0,0 +1,6 @@ +{ + "MonoBehaviour": { + "Version": 3, + "DisabledWarnings": "" + } +} diff --git a/ProjectSettings/PackageManagerSettings.asset b/ProjectSettings/PackageManagerSettings.asset new file mode 100644 index 0000000..80329fe --- /dev/null +++ b/ProjectSettings/PackageManagerSettings.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &1 +MonoBehaviour: + m_ObjectHideFlags: 61 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 13964, guid: 0000000000000000e000000000000000, type: 0} + m_Name: + m_EditorClassIdentifier: + m_EnablePreReleasePackages: 1 + m_EnablePackageDependencies: 0 + m_AdvancedSettingsExpanded: 1 + m_ScopedRegistriesSettingsExpanded: 1 + m_SeeAllPackageVersions: 0 + oneTimeWarningShown: 1 + m_Registries: + - m_Id: main + m_Name: + m_Url: https://packages.unity.com + m_Scopes: [] + m_IsDefault: 1 + m_Capabilities: 7 + m_UserSelectedRegistryName: + m_UserAddingNewScopedRegistry: 0 + m_RegistryInfoDraft: + m_ErrorMessage: + m_Original: + m_Id: + m_Name: + m_Url: + m_Scopes: [] + m_IsDefault: 0 + m_Capabilities: 0 + m_Modified: 0 + m_Name: + m_Url: + m_Scopes: + - + m_SelectedScopeIndex: 0 + m_LoadAssets: 0 diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index a4bd79c..85d4b36 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -3,7 +3,7 @@ --- !u!129 &1 PlayerSettings: m_ObjectHideFlags: 0 - serializedVersion: 20 + serializedVersion: 22 productGUID: 2024718c4027640389562cd9050f94ef AndroidProfiler: 0 AndroidFilterTouchesWhenObscured: 0 @@ -49,6 +49,8 @@ PlayerSettings: m_StereoRenderingPath: 0 m_ActiveColorSpace: 0 m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 m_StackTraceTypes: 010000000100000001000000010000000100000001000000 iosShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1 @@ -103,6 +105,7 @@ PlayerSettings: xboxOneMonoLoggingLevel: 0 xboxOneLoggingLevel: 1 xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 xboxOnePresentImmediateThreshold: 0 switchQueueCommandMemory: 0 switchQueueControlMemory: 16384 @@ -110,8 +113,15 @@ PlayerSettings: switchNVNShaderPoolsGranularity: 33554432 switchNVNDefaultPoolsGranularity: 16777216 switchNVNOtherPoolsGranularity: 16777216 + switchNVNMaxPublicTextureIDCount: 0 + switchNVNMaxPublicSamplerIDCount: 0 + stadiaPresentMode: 0 + stadiaTargetFramerate: 0 vulkanNumSwapchainBuffers: 3 vulkanEnableSetSRGBWrite: 0 + vulkanEnablePreTransform: 0 + vulkanEnableLateAcquireNextImage: 0 + vulkanEnableCommandBufferRecycling: 1 m_SupportedAspectRatios: 4:3: 1 5:4: 1 @@ -126,31 +136,6 @@ PlayerSettings: xboxOneDisableKinectGpuReservation: 1 xboxOneEnable7thCore: 1 vrSettings: - cardboard: - depthFormat: 0 - enableTransitionView: 0 - daydream: - depthFormat: 0 - useSustainedPerformanceMode: 0 - enableVideoLayer: 0 - useProtectedVideoMemory: 0 - minimumSupportedHeadTracking: 0 - maximumSupportedHeadTracking: 1 - hololens: - depthFormat: 1 - depthBufferSharingEnabled: 1 - lumin: - depthFormat: 0 - frameTiming: 2 - enableGLCache: 0 - glCacheMaxBlobSize: 524288 - glCacheMaxFileSize: 8388608 - oculus: - sharedDepthBuffer: 1 - dashSupport: 1 - lowOverheadMode: 0 - protectedContext: 0 - v2Signing: 1 enable360StereoCapture: 0 isWsaHolographicRemotingEnabled: 0 enableFrameTimingStats: 0 @@ -162,8 +147,12 @@ PlayerSettings: androidSupportedAspectRatio: 1 androidMaxAspectRatio: 2.1 applicationIdentifier: - Standalone: com.Company.ProductName - buildNumber: {} + Standalone: com.DefaultCompany.NewUnityProject1 + buildNumber: + Standalone: 0 + iPhone: 0 + tvOS: 0 + overrideDefaultApplicationIdentifier: 0 AndroidBundleVersionCode: 1 AndroidMinSdkVersion: 19 AndroidTargetSdkVersion: 0 @@ -180,32 +169,16 @@ PlayerSettings: StripUnusedMeshComponents: 1 VertexChannelCompressionMask: 4054 iPhoneSdkVersion: 988 - iOSTargetOSVersionString: 10.0 + iOSTargetOSVersionString: 11.0 tvOSSdkVersion: 0 tvOSRequireExtendedGameController: 0 - tvOSTargetOSVersionString: 10.0 + tvOSTargetOSVersionString: 11.0 uIPrerenderedIcon: 0 uIRequiresPersistentWiFi: 0 uIRequiresFullScreen: 1 uIStatusBarHidden: 1 uIExitOnSuspend: 0 uIStatusBarStyle: 0 - iPhoneSplashScreen: {fileID: 0} - iPhoneHighResSplashScreen: {fileID: 0} - iPhoneTallHighResSplashScreen: {fileID: 0} - iPhone47inSplashScreen: {fileID: 0} - iPhone55inPortraitSplashScreen: {fileID: 0} - iPhone55inLandscapeSplashScreen: {fileID: 0} - iPhone58inPortraitSplashScreen: {fileID: 0} - iPhone58inLandscapeSplashScreen: {fileID: 0} - iPadPortraitSplashScreen: {fileID: 0} - iPadHighResPortraitSplashScreen: {fileID: 0} - iPadLandscapeSplashScreen: {fileID: 0} - iPadHighResLandscapeSplashScreen: {fileID: 0} - iPhone65inPortraitSplashScreen: {fileID: 0} - iPhone65inLandscapeSplashScreen: {fileID: 0} - iPhone61inPortraitSplashScreen: {fileID: 0} - iPhone61inLandscapeSplashScreen: {fileID: 0} appleTVSplashScreen: {fileID: 0} appleTVSplashScreen2x: {fileID: 0} tvOSSmallIconLayers: [] @@ -233,8 +206,8 @@ PlayerSettings: iOSLaunchScreeniPadFillPct: 100 iOSLaunchScreeniPadSize: 100 iOSLaunchScreeniPadCustomXibPath: - iOSUseLaunchScreenStoryboard: 0 iOSLaunchScreenCustomStoryboardPath: + iOSLaunchScreeniPadCustomStoryboardPath: iOSDeviceRequirements: [] iOSURLSchemes: [] iOSBackgroundModes: 0 @@ -242,6 +215,7 @@ PlayerSettings: metalEditorSupport: 1 metalAPIValidation: 1 iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 appleDeveloperTeamID: iOSManualSigningProvisioningProfileID: tvOSManualSigningProvisioningProfileID: @@ -251,9 +225,17 @@ PlayerSettings: iOSRequireARKit: 0 iOSAutomaticallyDetectAndAddCapabilities: 1 appleEnableProMotion: 0 + shaderPrecisionModel: 0 clonedFromGUID: 5f34be1353de5cf4398729fda238591b templatePackageId: com.unity.template.2d@3.2.5 templateDefaultScene: Assets/Scenes/SampleScene.unity + useCustomMainManifest: 0 + useCustomLauncherManifest: 0 + useCustomMainGradleTemplate: 0 + useCustomLauncherGradleManifest: 0 + useCustomBaseGradleTemplate: 0 + useCustomGradlePropertiesTemplate: 0 + useCustomProguardFile: 0 AndroidTargetArchitectures: 1 AndroidSplashScreenScale: 0 androidSplashScreen: {fileID: 0} @@ -271,6 +253,9 @@ PlayerSettings: height: 180 banner: {fileID: 0} androidGamepadSupportLevel: 0 + AndroidMinifyWithR8: 0 + AndroidMinifyRelease: 0 + AndroidMinifyDebug: 0 AndroidValidateAppBundleSize: 1 AndroidAppBundleSizeToValidate: 150 m_BuildTargetIcons: [] @@ -312,6 +297,9 @@ PlayerSettings: - m_BuildTarget: AndroidPlayer m_APIs: 150000000b000000 m_Automatic: 0 + - m_BuildTarget: iOSSupport + m_APIs: 10000000 + m_Automatic: 1 m_BuildTargetVRSettings: [] openGLRequireES31: 0 openGLRequireES31AEP: 0 @@ -323,6 +311,7 @@ PlayerSettings: tvOS: 1 m_BuildTargetGroupLightmapEncodingQuality: [] m_BuildTargetGroupLightmapSettings: [] + m_BuildTargetNormalMapEncoding: [] playModeTestRunnerEnabled: 0 runPlayModeTestAsEditModeTest: 0 actionOnDotNetUnhandledException: 1 @@ -332,12 +321,15 @@ PlayerSettings: cameraUsageDescription: locationUsageDescription: microphoneUsageDescription: + switchNMETAOverride: switchNetLibKey: switchSocketMemoryPoolSize: 6144 switchSocketAllocatorPoolSize: 128 switchSocketConcurrencyLimit: 14 switchScreenResolutionBehavior: 2 switchUseCPUProfiler: 0 + switchUseGOLDLinker: 0 + switchLTOSetting: 0 switchApplicationID: 0x01004b9000490000 switchNSODependencies: switchTitleNames_0: @@ -355,6 +347,7 @@ PlayerSettings: switchTitleNames_12: switchTitleNames_13: switchTitleNames_14: + switchTitleNames_15: switchPublisherNames_0: switchPublisherNames_1: switchPublisherNames_2: @@ -370,6 +363,7 @@ PlayerSettings: switchPublisherNames_12: switchPublisherNames_13: switchPublisherNames_14: + switchPublisherNames_15: switchIcons_0: {fileID: 0} switchIcons_1: {fileID: 0} switchIcons_2: {fileID: 0} @@ -385,6 +379,7 @@ PlayerSettings: switchIcons_12: {fileID: 0} switchIcons_13: {fileID: 0} switchIcons_14: {fileID: 0} + switchIcons_15: {fileID: 0} switchSmallIcons_0: {fileID: 0} switchSmallIcons_1: {fileID: 0} switchSmallIcons_2: {fileID: 0} @@ -400,6 +395,7 @@ PlayerSettings: switchSmallIcons_12: {fileID: 0} switchSmallIcons_13: {fileID: 0} switchSmallIcons_14: {fileID: 0} + switchSmallIcons_15: {fileID: 0} switchManualHTML: switchAccessibleURLs: switchLegalInformation: @@ -431,6 +427,7 @@ PlayerSettings: switchRatingsInt_9: 0 switchRatingsInt_10: 0 switchRatingsInt_11: 0 + switchRatingsInt_12: 0 switchLocalCommunicationIds_0: switchLocalCommunicationIds_1: switchLocalCommunicationIds_2: @@ -461,6 +458,9 @@ PlayerSettings: switchSocketInitializeEnabled: 1 switchNetworkInterfaceManagerInitializeEnabled: 1 switchPlayerConnectionEnabled: 1 + switchUseNewStyleFilepaths: 0 + switchUseMicroSleepForYield: 1 + switchMicroSleepForYieldTime: 25 ps4NPAgeRating: 12 ps4NPTitleSecret: ps4NPTrophyPackPath: @@ -487,6 +487,7 @@ PlayerSettings: ps4ShareFilePath: ps4ShareOverlayImagePath: ps4PrivacyGuardImagePath: + ps4ExtraSceSysFile: ps4NPtitleDatPath: ps4RemotePlayKeyAssignment: -1 ps4RemotePlayKeyMappingDir: @@ -512,6 +513,7 @@ PlayerSettings: ps4UseResolutionFallback: 0 ps4ReprojectionSupport: 0 ps4UseAudio3dBackend: 0 + ps4UseLowGarlicFragmentationMode: 1 ps4SocialScreenEnabled: 0 ps4ScriptOptimizationLevel: 0 ps4Audio3dVirtualSpeakerCount: 14 @@ -528,6 +530,9 @@ PlayerSettings: ps4disableAutoHideSplash: 0 ps4videoRecordingFeaturesUsed: 0 ps4contentSearchFeaturesUsed: 0 + ps4CompatibilityPS5: 0 + ps4AllowPS5Detection: 0 + ps4GPU800MHz: 1 ps4attribEyeToEyeDistanceSettingVR: 0 ps4IncludedModules: [] ps4attribVROutputEnabled: 0 @@ -547,19 +552,26 @@ PlayerSettings: webGLAnalyzeBuildSize: 0 webGLUseEmbeddedResources: 0 webGLCompressionFormat: 1 + webGLWasmArithmeticExceptions: 0 webGLLinkerTarget: 1 webGLThreadsSupport: 0 - webGLWasmStreaming: 0 - scriptingDefineSymbols: {} + webGLDecompressionFallback: 0 + scriptingDefineSymbols: + 1: NATIVE_QUAD_TREE_ECS_USAGE + additionalCompilerArguments: {} platformArchitecture: {} scriptingBackend: {} il2cppCompilerConfiguration: {} managedStrippingLevel: {} incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 allowUnsafeCode: 1 + useDeterministicCompilation: 1 + enableRoslynAnalyzers: 1 additionalIl2CppArgs: scriptingRuntimeVersion: 1 gcIncremental: 0 + assemblyVersionValidation: 1 gcWBarrierValidation: 0 apiCompatibilityLevelPerPlatform: {} m_RenderingPath: 1 @@ -609,6 +621,7 @@ PlayerSettings: XboxOneCapability: [] XboxOneGameRating: {} XboxOneIsContentPackage: 0 + XboxOneEnhancedXboxCompatibilityMode: 0 XboxOneEnableGPUVariability: 1 XboxOneSockets: {} XboxOneSplashScreen: {fileID: 0} @@ -616,10 +629,8 @@ PlayerSettings: XboxOnePersistentLocalStorageSize: 0 XboxOneXTitleMemory: 8 XboxOneOverrideIdentityName: - vrEditorSettings: - daydream: - daydreamIconForeground: {fileID: 0} - daydreamIconBackground: {fileID: 0} + XboxOneOverrideIdentityPublisher: + vrEditorSettings: {} cloudServicesEnabled: UNet: 1 luminIcon: @@ -634,11 +645,12 @@ PlayerSettings: m_VersionCode: 1 m_VersionName: apiCompatibilityLevel: 6 + activeInputHandler: 0 cloudProjectId: framebufferDepthMemorylessMode: 0 + qualitySettingsNames: [] projectName: organizationId: cloudEnabled: 0 - enableNativePlatformBackendsForNewInputSystem: 0 - disableOldInputManagerSupport: 0 legacyClampBlendShapeWeights: 0 + virtualTexturingSupportEnabled: 0 diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index a381f6f..7066206 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2019.3.0b10 -m_EditorVersionWithRevision: 2019.3.0b10 (7955ac590a97) +m_EditorVersion: 2021.1.14f1 +m_EditorVersionWithRevision: 2021.1.14f1 (51d2f824827f) diff --git a/ProjectSettings/VersionControlSettings.asset b/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 0000000..dca2881 --- /dev/null +++ b/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,8 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_CollabEditorSettings: + inProgressEnabled: 1 diff --git a/ProjectSettings/XRPackageSettings.asset b/ProjectSettings/XRPackageSettings.asset new file mode 100644 index 0000000..7e791e1 --- /dev/null +++ b/ProjectSettings/XRPackageSettings.asset @@ -0,0 +1,5 @@ +{ + "m_Settings": [ + "RemoveLegacyInputHelpersForReload" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 09ba117..43ae834 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # NativeQuadtree A Quadtree Native Collection for Unity DOTS. Octree version is here: https://github.com/marijnz/NativeOctree + + ## Implementation - It's a DOTS native container, meaning it's handling its own unmanaged memory and can be passed into jobs! - It currently only supports the storing of points diff --git a/UserSettings/EditorUserSettings.asset b/UserSettings/EditorUserSettings.asset new file mode 100644 index 0000000..a68cd3d --- /dev/null +++ b/UserSettings/EditorUserSettings.asset @@ -0,0 +1,30 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!162 &1 +EditorUserSettings: + m_ObjectHideFlags: 0 + serializedVersion: 4 + m_ConfigSettings: + Advanced Settings: + value: 183b144645154b6805011b0314355e1e15121c1d233a2a343e6b4773e4e1382be78d2b + flags: 0 + Entity Inspector Settings: + value: 183b144645154b790c0d07271e271d4a564654406c6866706f0d1420f2ec3521c1e83bf9e8343a322d36f62c017c5b7ff40b0518f36117 + flags: 0 + RecentlyUsedScenePath-0: + value: 224247031146467e150f01321c26102413040c6a1f2b233e2867083debf42d + flags: 0 + vcSharedLogLevel: + value: 0d5e400f0650 + flags: 0 + m_VCAutomaticAdd: 1 + m_VCDebugCom: 0 + m_VCDebugCmd: 0 + m_VCDebugOut: 0 + m_SemanticMergeMode: 2 + m_VCShowFailedCheckout: 1 + m_VCOverwriteFailedCheckoutAssets: 1 + m_VCProjectOverlayIcons: 1 + m_VCHierarchyOverlayIcons: 1 + m_VCOtherOverlayIcons: 1 + m_VCAllowAsyncUpdate: 1 diff --git a/UserSettings/Search.settings b/UserSettings/Search.settings new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/UserSettings/Search.settings @@ -0,0 +1 @@ +{} \ No newline at end of file