From c8e7088e202557c6ff82fefb4554171ab69ceda9 Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 18 Jul 2021 15:18:06 +0100 Subject: [PATCH 01/16] Fixed tests, added assembly definitions, got compiling with up to date pacakges --- Assets/Editor/NativeQuadTree.Editor.asmdef | 21 +++++++++++++++++++ .../Editor/NativeQuadTree.Editor.asmdef.meta | 7 +++++++ Assets/Editor/QuadTreeDrawer.cs | 4 +++- Assets/Editor/QuadTreeTests.cs | 11 +++++----- Assets/NativeQuadTree.asmdef | 19 +++++++++++++++++ Assets/NativeQuadTree.asmdef.meta | 7 +++++++ Assets/NativeQuadTree.cs | 2 +- Assets/package.json | 20 ++++++++++++++++++ Assets/package.json.meta | 7 +++++++ 9 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 Assets/Editor/NativeQuadTree.Editor.asmdef create mode 100644 Assets/Editor/NativeQuadTree.Editor.asmdef.meta create mode 100644 Assets/NativeQuadTree.asmdef create mode 100644 Assets/NativeQuadTree.asmdef.meta create mode 100644 Assets/package.json create mode 100644 Assets/package.json.meta diff --git a/Assets/Editor/NativeQuadTree.Editor.asmdef b/Assets/Editor/NativeQuadTree.Editor.asmdef new file mode 100644 index 0000000..39b122a --- /dev/null +++ b/Assets/Editor/NativeQuadTree.Editor.asmdef @@ -0,0 +1,21 @@ +{ + "name": "NativeQuadTree.Editor", + "rootNamespace": "", + "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..365c05e 100644 --- a/Assets/Editor/QuadTreeDrawer.cs +++ b/Assets/Editor/QuadTreeDrawer.cs @@ -14,7 +14,9 @@ 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 diff --git a/Assets/Editor/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 79d774b..9dfb00e 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -32,7 +32,7 @@ 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++) { @@ -103,13 +103,10 @@ public void InsertTriggerDivideNonBurstBulk() { var values = GetValues(); - var positions = new NativeArray(values.Length, Allocator.TempJob); + var positions = new NativeArray(values, Allocator.Persistent); var quadTree = new NativeQuadTree(Bounds); - 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++) { @@ -128,7 +125,9 @@ public void InsertTriggerDivideNonBurstBulk() Debug.Log(s.Elapsed.TotalMilliseconds); QuadTreeDrawer.Draw(quadTree); + quadTree.Dispose(); positions.Dispose(); + elements.Dispose(); } } diff --git a/Assets/NativeQuadTree.asmdef b/Assets/NativeQuadTree.asmdef new file mode 100644 index 0000000..1066e2b --- /dev/null +++ b/Assets/NativeQuadTree.asmdef @@ -0,0 +1,19 @@ +{ + "name": "NativeQuadTree", + "rootNamespace": "", + "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..6d41027 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -75,7 +75,7 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m } #if ENABLE_UNITY_COLLECTIONS_CHECKS - CollectionHelper.CheckIsUnmanaged(); + //CollectionHelper.CheckIsUnmanaged(); DisposeSentinel.Create(out safetyHandle, out disposeSentinel, 1, allocator); #endif 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: From b3dbe3329597cb0d7ef48469850bb366333d760f Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 18 Jul 2021 18:42:22 +0100 Subject: [PATCH 02/16] Added Circle Range Query Type --- Assets/Circle2D.cs | 39 +++++++ ...reeRangeQuery.cs.meta => Circle2D.cs.meta} | 2 +- Assets/NativeQuadTree.cs | 10 +- Assets/QuadTreeCircleRangeQuery.cs | 110 ++++++++++++++++++ Assets/QuadTreeCircleRangeQuery.cs.meta | 11 ++ ...angeQuery.cs => QuadTreeRectRangeQuery.cs} | 3 +- Assets/QuadTreeRectRangeQuery.cs.meta | 11 ++ 7 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 Assets/Circle2D.cs rename Assets/{NativeQuadTreeRangeQuery.cs.meta => Circle2D.cs.meta} (83%) create mode 100644 Assets/QuadTreeCircleRangeQuery.cs create mode 100644 Assets/QuadTreeCircleRangeQuery.cs.meta rename Assets/{NativeQuadTreeRangeQuery.cs => QuadTreeRectRangeQuery.cs} (99%) create mode 100644 Assets/QuadTreeRectRangeQuery.cs.meta diff --git a/Assets/Circle2D.cs b/Assets/Circle2D.cs new file mode 100644 index 0000000..522c5b5 --- /dev/null +++ b/Assets/Circle2D.cs @@ -0,0 +1,39 @@ +using NativeQuadTree; +using Unity.Mathematics; + +namespace NativeQuadTree +{ + public class Circle2D + { + public float2 Center; + public float Radious; + + public bool Contains(float2 point) + { + return math.distance(point, Center) <= Radious; + } + + 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) + { + float2 center = new float2() + { + x = math.clamp(Center.x, b.Center.x - b.Extents.x, b.Center.x + b.Extents.x), + y = math.clamp(Center.y, b.Center.y - b.Extents.y, b.Center.y + b.Extents.y) + }; + + return Contains(center); + } + + public bool Intersects(Circle2D b) + { + return math.distance(Center, b.Center) <= Radious + 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/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 6d41027..2d2d4cd 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -209,7 +209,15 @@ public void RangeQuery(AABB2D bounds, NativeList> results) #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); #endif - new QuadTreeRangeQuery().Query(this, bounds, results); + new QuadTreeRectRangeQuery().Query(this, bounds, results); + } + + public void RangeQuery(Circle2D bounds, NativeList> results) + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); +#endif + new QuadTreeCircleRangeQuery().Query(this, bounds, results); } public void Clear() diff --git a/Assets/QuadTreeCircleRangeQuery.cs b/Assets/QuadTreeCircleRangeQuery.cs new file mode 100644 index 0000000..542fd27 --- /dev/null +++ b/Assets/QuadTreeCircleRangeQuery.cs @@ -0,0 +1,110 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace NativeQuadTree +{ + public unsafe partial struct NativeQuadTree where T : unmanaged + { + struct QuadTreeCircleRangeQuery + { + NativeQuadTree tree; + + UnsafeList* fastResults; + int count; + + 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) + { + 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/QuadTreeCircleRangeQuery.cs.meta b/Assets/QuadTreeCircleRangeQuery.cs.meta new file mode 100644 index 0000000..aa06e11 --- /dev/null +++ b/Assets/QuadTreeCircleRangeQuery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70baadcd4ca8cf84b8281fd7cbf41c37 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/NativeQuadTreeRangeQuery.cs b/Assets/QuadTreeRectRangeQuery.cs similarity index 99% rename from Assets/NativeQuadTreeRangeQuery.cs rename to Assets/QuadTreeRectRangeQuery.cs index a460c9a..0832619 100644 --- a/Assets/NativeQuadTreeRangeQuery.cs +++ b/Assets/QuadTreeRectRangeQuery.cs @@ -7,7 +7,7 @@ namespace NativeQuadTree { public unsafe partial struct NativeQuadTree where T : unmanaged { - struct QuadTreeRangeQuery + struct QuadTreeRectRangeQuery { NativeQuadTree tree; @@ -105,6 +105,5 @@ static AABB2D GetChildBounds(AABB2D parentBounds, int childZIndex) } } } - } } \ No newline at end of file diff --git a/Assets/QuadTreeRectRangeQuery.cs.meta b/Assets/QuadTreeRectRangeQuery.cs.meta new file mode 100644 index 0000000..05a3062 --- /dev/null +++ b/Assets/QuadTreeRectRangeQuery.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39acf01271df42e45ae1cd07020783c0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From ffc987462c624acb3e6ee91599cda1409e70d321 Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 18 Jul 2021 19:29:42 +0100 Subject: [PATCH 03/16] Allow better native compatability --- Assets/Circle2D.cs | 7 ++++++- Assets/NativeQuadTree.cs | 14 +++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Assets/Circle2D.cs b/Assets/Circle2D.cs index 522c5b5..8f5621b 100644 --- a/Assets/Circle2D.cs +++ b/Assets/Circle2D.cs @@ -1,18 +1,21 @@ +using System.Runtime.CompilerServices; using NativeQuadTree; using Unity.Mathematics; namespace NativeQuadTree { - public class Circle2D + public struct Circle2D { public float2 Center; public float Radious; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Contains(float2 point) { return math.distance(point, Center) <= Radious; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] 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)) && @@ -20,6 +23,7 @@ public bool Contains(AABB2D b) { Contains(b.Center + new float2(b.Extents.x, b.Extents.y)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(AABB2D b) { float2 center = new float2() @@ -31,6 +35,7 @@ public bool Intersects(AABB2D b) return Contains(center); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(Circle2D b) { return math.distance(Center, b.Center) <= Radious + b.Radious; diff --git a/Assets/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 2d2d4cd..16022a5 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -31,7 +31,7 @@ struct QuadNode /// public unsafe partial 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] @@ -74,7 +74,7 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m throw new InvalidOperationException(); } -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE //CollectionHelper.CheckIsUnmanaged(); DisposeSentinel.Create(out safetyHandle, out disposeSentinel, 1, allocator); #endif @@ -106,7 +106,7 @@ public void ClearAndBulkInsert(NativeArray> incomingElements) // for existing data. Clear(); -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif @@ -206,7 +206,7 @@ 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); @@ -214,7 +214,7 @@ public void RangeQuery(AABB2D bounds, NativeList> results) public void RangeQuery(Circle2D bounds, NativeList> results) { -#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); #endif new QuadTreeCircleRangeQuery().Query(this, bounds, results); @@ -222,7 +222,7 @@ public void RangeQuery(Circle2D bounds, NativeList> 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()); @@ -239,7 +239,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 } From 5f3981f5ae8d5e282a883c51eb7fad0d2ca43aa2 Mon Sep 17 00:00:00 2001 From: Crener Date: Mon, 16 Aug 2021 01:55:24 +0100 Subject: [PATCH 04/16] Improved burst compatability, alternate way to bulk add entries with more parallelism --- Assets/Editor/QuadTreeTests.cs | 7 +- Assets/NativeQuadTree.cs | 142 +++++++++++----- Assets/NativeQuadTreeDrawing.cs | 33 ++-- Assets/NativeQuadTreeParallelAdd.cs | 196 +++++++++++++++++++++++ Assets/NativeQuadTreeParallelAdd.cs.meta | 11 ++ Assets/QuadTreeJobs.cs | 15 +- 6 files changed, 342 insertions(+), 62 deletions(-) create mode 100644 Assets/NativeQuadTreeParallelAdd.cs create mode 100644 Assets/NativeQuadTreeParallelAdd.cs.meta diff --git a/Assets/Editor/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 9dfb00e..0389015 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -43,10 +43,12 @@ public void InsertTriggerDivideBulk() }; } + NativeReference> data = new NativeReference>(Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + data.Value = new NativeQuadTree(Bounds); var job = new QuadTreeJobs.AddBulkJob { Elements = elements, - QuadTree = new NativeQuadTree(Bounds) + QuadTree = data }; var s = Stopwatch.StartNew(); @@ -56,8 +58,9 @@ public void InsertTriggerDivideBulk() s.Stop(); Debug.Log(s.Elapsed.TotalMilliseconds); - QuadTreeDrawer.Draw(job.QuadTree); + QuadTreeDrawer.Draw(data.Value); job.QuadTree.Dispose(); + data.Dispose(); elements.Dispose(); } diff --git a/Assets/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 16022a5..abb6d17 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -1,7 +1,12 @@ using System; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; +using Mono.Cecil.Cil; +using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; +using UnityEngine.Assertions; namespace NativeQuadTree { @@ -18,7 +23,7 @@ struct QuadNode public int firstChildIndex; // Number of elements in the leaf - public short count; + public ushort count; public bool isLeaf; } @@ -47,10 +52,11 @@ public unsafe partial struct NativeQuadTree : IDisposable where T : unmanaged [NativeDisableUnsafePtrRestriction] UnsafeList* nodes; + public int EntryCount => elementsCount; int elementsCount; int maxDepth; - short maxLeafElements; + ushort maxLeafElements; AABB2D bounds; // NOTE: Currently assuming uniform @@ -59,7 +65,7 @@ public unsafe partial struct NativeQuadTree : IDisposable where T : unmanaged /// - 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() { @@ -78,9 +84,16 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m //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(), @@ -101,6 +114,22 @@ 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(); + } + + private void InitialiseBulkInsert(NativeArray> incomingElements) { // Always have to clear before bulk insert as otherwise the lookup and node allocations need to account // for existing data. @@ -109,30 +138,86 @@ public void ClearAndBulkInsert(NativeArray> incomingElements) #if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif +#if UNITY_ASSERTIONS + //int totalSize = LookupTables.DepthSizeLookup[maxDepth+1]; + //Assert.IsTrue(totalSize >= incomingElements.Length, $"Quad tree size is limited to {totalSize}, attempting to store {incomingElements.Length} items"); +#endif // Resize if needed if(elements->Capacity < elementsCount + incomingElements.Length) { elements->Resize>(math.max(incomingElements.Length, elements->Capacity*2)); } + } - // 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] + private void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray> incomingElements) + { + for (int i = 0; i < incomingElements.Length; i++) + { + int atIndex = 0; + for (int depth = 0; depth <= maxDepth; depth++) + { + QuadNode node = UnsafeUtility.ReadArrayElement(nodes->Ptr, atIndex); + if(node.isLeaf) + { + #if UNITY_ASSERTIONS && !ENABLE_BURST_AOT + if(node.count > maxLeafElements) + { + // the allocation done in the constructor limits the amount of elements in each leaf + AssertIsTrue(false, "Quad Tree node {0} is filled with elements, consider allocating a larger leaf node size than {1}", atIndex, maxLeafElements); + } + #endif + + // 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); + } + } + } + + [BurstDiscard, StringFormatMethod("message")] + private static void AssertIsTrue(bool condition, string message, params object[] parts) + { + Assert.IsTrue(condition, string.Format(message, parts)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private NativeArray PrepairMortonCodes(NativeArray> incomingElements) + { + NativeArray mortonCodes = new NativeArray(incomingElements.Length, Allocator.Temp); + PrepairMortonCodesInitial(incomingElements, mortonCodes); + PrepairMortonCodesIndex(mortonCodes); + return mortonCodes; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void PrepairMortonCodesInitial(NativeArray> incomingElements, NativeArray mortonCodes) + { + float2 depthExtentsScaling = LookupTables.DepthLookup[maxDepth] / bounds.Extents; + for (int i = 0; i < incomingElements.Length; i++) { - var incPos = incomingElements[i].pos; + 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)] + private 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++) @@ -142,37 +227,12 @@ public void ClearAndBulkInsert(NativeArray> incomingElements) 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(); } + [BurstCompatible] int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) { - var atDepth = math.max(0, maxDepth - depth); + int atDepth = math.max(0, 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) @@ -185,9 +245,9 @@ void RecursivePrepareLeaves(int prevOffset, int depth) { for (int l = 0; l < 4; l++) { - var at = prevOffset + l * LookupTables.DepthSizeLookup[maxDepth - depth+1]; + int at = prevOffset + l * LookupTables.DepthSizeLookup[maxDepth - depth+1]; - var elementCount = UnsafeUtility.ReadArrayElement(lookup->Ptr, at); + int elementCount = UnsafeUtility.ReadArrayElement(lookup->Ptr, at); if(elementCount > maxLeafElements && depth < maxDepth) { @@ -197,7 +257,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; } diff --git a/Assets/NativeQuadTreeDrawing.cs b/Assets/NativeQuadTreeDrawing.cs index 5f056d2..c850f7e 100644 --- a/Assets/NativeQuadTreeDrawing.cs +++ b/Assets/NativeQuadTreeDrawing.cs @@ -10,24 +10,23 @@ namespace NativeQuadTree /// public unsafe partial struct NativeQuadTree where T : unmanaged { - public static void Draw(NativeQuadTree tree, NativeList> results, AABB2D range, - Color[][] texture) + 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; + float widthMult = texture.Length / tree.bounds.Extents.x * 2 / 2 / 2; + float 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; + 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++) { - var node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, i); + QuadNode node = UnsafeUtility.ReadArrayElement(tree.nodes->Ptr, i); if(node.count > 0) { for (int k = 0; k < node.count; k++) { - var element = + QuadElement element = UnsafeUtility.ReadArrayElement>(tree.elements->Ptr, node.firstChildIndex + k); texture[(int) ((element.pos.x + widthAdd) * widthMult)] @@ -36,7 +35,7 @@ public static void Draw(NativeQuadTree tree, NativeList> resul } } - foreach (var element in results) + foreach (QuadElement element in results) { texture[(int) ((element.pos.x + widthAdd) * widthMult)] [(int) ((element.pos.y + heightAdd) * heightMult)] = Color.green; @@ -47,25 +46,25 @@ public static void Draw(NativeQuadTree tree, NativeList> resul 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; + float widthMult = texture.Length / tree.bounds.Extents.x * 2 / 2 / 2; + float 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; + float widthAdd = tree.bounds.Center.x + tree.bounds.Extents.x; + float 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); + 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++) { - var poxX = left.x + 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++) { - var posY = top.y + 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; } diff --git a/Assets/NativeQuadTreeParallelAdd.cs b/Assets/NativeQuadTreeParallelAdd.cs new file mode 100644 index 0000000..dd92e95 --- /dev/null +++ b/Assets/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 +{ + public unsafe partial struct NativeQuadTree 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.ScheduleBatch(quadElementArray.Length, threadBucketSize, initHandle); + //JobHandle morton1Handle = mortonCreate.Schedule(initHandle); + JobHandle morton2Handle = mortonIndex.ScheduleBatch(quadElementArray.Length, threadBucketSize, morton1Handle); + //JobHandle morton2Handle = mortonIndex.Schedule(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 : 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 struct IndexMortonCodesJob : IJobParallelForBatch + { + 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/NativeQuadTreeParallelAdd.cs.meta b/Assets/NativeQuadTreeParallelAdd.cs.meta new file mode 100644 index 0000000..08ca5f7 --- /dev/null +++ b/Assets/NativeQuadTreeParallelAdd.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 56445f3bae4e1de4dab564fcffa352a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/QuadTreeJobs.cs b/Assets/QuadTreeJobs.cs index 0c303e2..d5606d0 100644 --- a/Assets/QuadTreeJobs.cs +++ b/Assets/QuadTreeJobs.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using Unity.Burst; using Unity.Collections; using Unity.Jobs; @@ -18,11 +19,21 @@ public struct AddBulkJob : IJob where T : unmanaged [ReadOnly] public NativeArray> Elements; - public NativeQuadTree QuadTree; + public NativeReference> QuadTree; public void Execute() { - QuadTree.ClearAndBulkInsert(Elements); + 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"); } } From d7a8e49a04dad8c35518127adc666a95685bf43e Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 22 Aug 2021 21:29:24 +0100 Subject: [PATCH 05/16] Major organisation of file structure --- Assets/Editor/QuadTreeDrawer.cs | 7 +- Assets/Editor/QuadTreeTests.cs | 17 +-- Assets/Helpers.meta | 8 ++ Assets/{ => Helpers}/LookupTables.cs | 0 Assets/Helpers/LookupTables.cs.meta | 11 ++ .../NativeQuadTreeDrawHelpers.cs} | 12 +- .../Helpers/NativeQuadTreeDrawHelpers.cs.meta | 11 ++ Assets/Jobs.meta | 8 ++ Assets/Jobs/AddBulkJob.cs | 34 ++++++ Assets/Jobs/AddBulkJob.cs.meta | 11 ++ Assets/Jobs/Internal.meta | 8 ++ .../Jobs/Internal/QuadTreeCircleRangeQuery.cs | 106 +++++++++++++++++ .../QuadTreeCircleRangeQuery.cs.meta | 2 +- .../Jobs/Internal/QuadTreeRectRangeQuery.cs | 106 +++++++++++++++++ .../Internal}/QuadTreeRectRangeQuery.cs.meta | 2 +- .../{ => Jobs}/NativeQuadTreeParallelAdd.cs | 18 +-- .../NativeQuadTreeParallelAdd.cs.meta | 2 +- Assets/Jobs/RangeQueryJob.cs | 32 +++++ Assets/Jobs/RangeQueryJob.cs.meta | 11 ++ Assets/LookupTables.cs.meta | 3 - Assets/NativeQuadTree.cs | 76 +++++------- Assets/NativeQuadTreeDrawing.cs.meta | 3 - Assets/QuadElement.cs | 13 +++ Assets/QuadElement.cs.meta | 3 + Assets/QuadNode.cs | 12 ++ Assets/QuadNode.cs.meta | 3 + Assets/QuadTreeCircleRangeQuery.cs | 110 ------------------ Assets/QuadTreeJobs.cs | 65 ----------- Assets/QuadTreeJobs.cs.meta | 3 - Assets/QuadTreeRectRangeQuery.cs | 109 ----------------- 30 files changed, 439 insertions(+), 367 deletions(-) create mode 100644 Assets/Helpers.meta rename Assets/{ => Helpers}/LookupTables.cs (100%) create mode 100644 Assets/Helpers/LookupTables.cs.meta rename Assets/{NativeQuadTreeDrawing.cs => Helpers/NativeQuadTreeDrawHelpers.cs} (85%) create mode 100644 Assets/Helpers/NativeQuadTreeDrawHelpers.cs.meta create mode 100644 Assets/Jobs.meta create mode 100644 Assets/Jobs/AddBulkJob.cs create mode 100644 Assets/Jobs/AddBulkJob.cs.meta create mode 100644 Assets/Jobs/Internal.meta create mode 100644 Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs rename Assets/{ => Jobs/Internal}/QuadTreeCircleRangeQuery.cs.meta (83%) create mode 100644 Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs rename Assets/{ => Jobs/Internal}/QuadTreeRectRangeQuery.cs.meta (83%) rename Assets/{ => Jobs}/NativeQuadTreeParallelAdd.cs (93%) rename Assets/{ => Jobs}/NativeQuadTreeParallelAdd.cs.meta (83%) create mode 100644 Assets/Jobs/RangeQueryJob.cs create mode 100644 Assets/Jobs/RangeQueryJob.cs.meta delete mode 100644 Assets/LookupTables.cs.meta delete mode 100644 Assets/NativeQuadTreeDrawing.cs.meta create mode 100644 Assets/QuadElement.cs create mode 100644 Assets/QuadElement.cs.meta create mode 100644 Assets/QuadNode.cs create mode 100644 Assets/QuadNode.cs.meta delete mode 100644 Assets/QuadTreeCircleRangeQuery.cs delete mode 100644 Assets/QuadTreeJobs.cs delete mode 100644 Assets/QuadTreeJobs.cs.meta delete mode 100644 Assets/QuadTreeRectRangeQuery.cs diff --git a/Assets/Editor/QuadTreeDrawer.cs b/Assets/Editor/QuadTreeDrawer.cs index 365c05e..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; @@ -19,7 +20,7 @@ public static void Draw(NativeQuadTree quadTree) where T : unmanaged 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); @@ -35,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 0389015..544f54b 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using NUnit.Framework; using NativeQuadTree; +using NativeQuadTree.Jobs; using Unity.Burst; using Unity.Collections; using Unity.Jobs; @@ -38,14 +39,14 @@ public void InsertTriggerDivideBulk() { elements[i] = new QuadElement { - pos = values[i], - element = i + Pos = values[i], + Element = i }; } NativeReference> data = new NativeReference>(Allocator.TempJob, NativeArrayOptions.UninitializedMemory); data.Value = new NativeQuadTree(Bounds); - var job = new QuadTreeJobs.AddBulkJob + var job = new AddBulkJob { Elements = elements, QuadTree = data @@ -75,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); quadTree.ClearAndBulkInsert(elements); - var queryJob = new QuadTreeJobs.RangeQueryJob + var queryJob = new RangeQueryJob { QuadTree = quadTree, Bounds = new AABB2D(100, 140), @@ -115,8 +116,8 @@ public void InsertTriggerDivideNonBurstBulk() { elements[i] = new QuadElement { - pos = positions[i], - element = i + Pos = positions[i], + Element = i }; } 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/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/NativeQuadTreeDrawing.cs b/Assets/Helpers/NativeQuadTreeDrawHelpers.cs similarity index 85% rename from Assets/NativeQuadTreeDrawing.cs rename to Assets/Helpers/NativeQuadTreeDrawHelpers.cs index c850f7e..a7d8597 100644 --- a/Assets/NativeQuadTreeDrawing.cs +++ b/Assets/Helpers/NativeQuadTreeDrawHelpers.cs @@ -8,7 +8,7 @@ namespace NativeQuadTree /// /// Editor drawing of the NativeQuadTree /// - public unsafe partial struct NativeQuadTree where T : unmanaged + public unsafe struct NativeQuadTreeDrawHelpers where T : unmanaged { public static void Draw(NativeQuadTree tree, NativeList> results, AABB2D range, Color[][] texture) { @@ -29,22 +29,22 @@ public static void Draw(NativeQuadTree tree, NativeList> resul 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; + 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; + 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) + 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; 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/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/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..c5e07c7 --- /dev/null +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -0,0 +1,106 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace NativeQuadTree.Jobs.Internal +{ + internal unsafe struct QuadTreeCircleRangeQuery where T : unmanaged + { + private NativeQuadTree tree; + + 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) + { + 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); + } + } + } + } + } + } + + private 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/QuadTreeCircleRangeQuery.cs.meta b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs.meta similarity index 83% rename from Assets/QuadTreeCircleRangeQuery.cs.meta rename to Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs.meta index aa06e11..697c1ce 100644 --- a/Assets/QuadTreeCircleRangeQuery.cs.meta +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 70baadcd4ca8cf84b8281fd7cbf41c37 +guid: 7ab7a0df3db3adc4691537d30eb681f9 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs new file mode 100644 index 0000000..4f83c97 --- /dev/null +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs @@ -0,0 +1,106 @@ +using System; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; + +namespace NativeQuadTree.Jobs.Internal +{ + internal unsafe struct QuadTreeRectRangeQuery where T : unmanaged + { + private NativeQuadTree tree; + + 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) + { + 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); + } + } + } + } + } + } + + private 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(); + } + } + } +} diff --git a/Assets/QuadTreeRectRangeQuery.cs.meta b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs.meta similarity index 83% rename from Assets/QuadTreeRectRangeQuery.cs.meta rename to Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs.meta index 05a3062..7a0002b 100644 --- a/Assets/QuadTreeRectRangeQuery.cs.meta +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 39acf01271df42e45ae1cd07020783c0 +guid: e67c58fd3f0945545894dd04b8bc5e53 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/NativeQuadTreeParallelAdd.cs b/Assets/Jobs/NativeQuadTreeParallelAdd.cs similarity index 93% rename from Assets/NativeQuadTreeParallelAdd.cs rename to Assets/Jobs/NativeQuadTreeParallelAdd.cs index dd92e95..4285bb5 100644 --- a/Assets/NativeQuadTreeParallelAdd.cs +++ b/Assets/Jobs/NativeQuadTreeParallelAdd.cs @@ -5,9 +5,9 @@ using Unity.Jobs; using Unity.Mathematics; -namespace NativeQuadTree +namespace NativeQuadTree.Jobs { - public unsafe partial struct NativeQuadTree where T : unmanaged + public static class NativeQuadTreeParallelAdd where T : unmanaged { public static JobHandle SetupBulkAddJobChain(NativeReference> tree, NativeArray> quadElementArray, JobHandle dependency) { @@ -94,10 +94,10 @@ public void Execute() public void Execute(int startIndex, int count) { NativeQuadTree quadTree = QuadTree.Value; - float2 depthExtentsScaling = LookupTables.DepthLookup[quadTree.maxDepth] / quadTree.bounds.Extents; + float2 depthExtentsScaling = LookupTables.DepthLookup[quadTree.MaxDepth] / quadTree.bounds.Extents; for (int i = startIndex; i < startIndex + count; i++) { - float2 incPos = Elements[i].pos; + 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 @@ -110,7 +110,7 @@ public void Execute(int startIndex, int count) } [BurstCompile] - private struct IndexMortonCodesJob : IJobParallelForBatch + private unsafe struct IndexMortonCodesJob : IJobParallelForBatch { public NativeArray MortonCodes; [ReadOnly] @@ -129,7 +129,7 @@ public void Execute(int startIndex, int count) for (int i = startIndex; i < startIndex + count; i++) { int atIndex = 0; - for (int depth = 0; depth <= quadTree.maxDepth; depth++) + 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)))++; @@ -168,13 +168,13 @@ public void Execute() QuadTree.Value = quadTree; } - public void Execute(int startIndex, int count) + /*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++) + for (int depth = 0; depth <= quadTree.MaxDepth; depth++) { QuadNode node = UnsafeUtility.ReadArrayElement(quadTree.nodes->Ptr, atIndex); if(node.isLeaf) @@ -190,7 +190,7 @@ public void Execute(int startIndex, int count) atIndex = quadTree.IncrementIndex(depth, MortonCodes, i, atIndex); } } - } + }*/ } } } \ No newline at end of file diff --git a/Assets/NativeQuadTreeParallelAdd.cs.meta b/Assets/Jobs/NativeQuadTreeParallelAdd.cs.meta similarity index 83% rename from Assets/NativeQuadTreeParallelAdd.cs.meta rename to Assets/Jobs/NativeQuadTreeParallelAdd.cs.meta index 08ca5f7..8f12b1d 100644 --- a/Assets/NativeQuadTreeParallelAdd.cs.meta +++ b/Assets/Jobs/NativeQuadTreeParallelAdd.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 56445f3bae4e1de4dab564fcffa352a8 +guid: 99fdf95183d0d8644b8b483643f8d29d MonoImporter: externalObjects: {} serializedVersion: 2 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/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.cs b/Assets/NativeQuadTree.cs index abb6d17..cafd176 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -2,6 +2,7 @@ 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; @@ -10,23 +11,6 @@ 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 ushort count; - public bool isLeaf; - } - /// /// A QuadTree aimed to be used with Burst, supports fast bulk insertion and querying. /// @@ -34,7 +18,7 @@ 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 && !NATIVE_QUAD_TREE_ECS_USAGE // Safety @@ -44,21 +28,23 @@ 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; public int EntryCount => elementsCount; - int elementsCount; + private int elementsCount; - int maxDepth; - ushort 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. @@ -70,7 +56,7 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m ) : this() { this.bounds = bounds; - this.maxDepth = maxDepth; + this.m_maxDepth = maxDepth; this.maxLeafElements = maxLeafElements; elementsCount = 0; @@ -129,7 +115,7 @@ public void ClearAndBulkInsert(NativeArray> incomingElements) mortonCodes.Dispose(); } - private void InitialiseBulkInsert(NativeArray> incomingElements) + 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. @@ -151,21 +137,21 @@ private void InitialiseBulkInsert(NativeArray> incomingElements) } [BurstCompatible] - private void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray> incomingElements) + internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray> incomingElements) { for (int i = 0; i < incomingElements.Length; i++) { int atIndex = 0; - for (int depth = 0; depth <= maxDepth; depth++) + for (int depth = 0; depth <= m_maxDepth; depth++) { QuadNode node = UnsafeUtility.ReadArrayElement(nodes->Ptr, atIndex); if(node.isLeaf) { - #if UNITY_ASSERTIONS && !ENABLE_BURST_AOT + #if UNITY_ASSERTIONS if(node.count > maxLeafElements) { // the allocation done in the constructor limits the amount of elements in each leaf - AssertIsTrue(false, "Quad Tree node {0} is filled with elements, consider allocating a larger leaf node size than {1}", atIndex, maxLeafElements); + AssertIsTrue(false, "Quad Tree node " + atIndex + " is filled with elements, consider allocating a larger leaf node size than " + maxLeafElements); } #endif @@ -182,9 +168,9 @@ private void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray PrepairMortonCodes(NativeArray> incoming } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PrepairMortonCodesInitial(NativeArray> incomingElements, NativeArray mortonCodes) + internal void PrepairMortonCodesInitial(NativeArray> incomingElements, NativeArray mortonCodes) { - float2 depthExtentsScaling = LookupTables.DepthLookup[maxDepth] / bounds.Extents; + float2 depthExtentsScaling = LookupTables.DepthLookup[m_maxDepth] / bounds.Extents; for (int i = 0; i < incomingElements.Length; i++) { - float2 incPos = incomingElements[i].pos; + float2 incPos = incomingElements[i].Pos; incPos -= bounds.Center; // Offset by center incPos.y = -incPos.y; // World -> array float2 pos = (incPos + bounds.Extents) * .5f; // Make positive @@ -214,13 +200,13 @@ private void PrepairMortonCodesInitial(NativeArray> incomingEleme } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PrepairMortonCodesIndex(NativeArray mortonCodes) + internal void PrepairMortonCodesIndex(NativeArray mortonCodes) { // Index total child element count per node (total, so parent's counts include those of child nodes) 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)))++; @@ -230,9 +216,9 @@ private void PrepairMortonCodesIndex(NativeArray mortonCodes) } [BurstCompatible] - int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) + internal int IncrementIndex(int depth, NativeArray mortonCodes, int i, int atIndex) { - int 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) @@ -241,15 +227,15 @@ 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) { for (int l = 0; l < 4; l++) { - int at = prevOffset + l * LookupTables.DepthSizeLookup[maxDepth - depth+1]; + 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); @@ -269,7 +255,7 @@ public void RangeQuery(AABB2D bounds, NativeList> results) #if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); #endif - new QuadTreeRectRangeQuery().Query(this, bounds, results); + new QuadTreeRectRangeQuery().Query(this, bounds, results); } public void RangeQuery(Circle2D bounds, NativeList> results) @@ -277,7 +263,7 @@ public void RangeQuery(Circle2D bounds, NativeList> results) #if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckReadAndThrow(safetyHandle); #endif - new QuadTreeCircleRangeQuery().Query(this, bounds, results); + new QuadTreeCircleRangeQuery().Query(this, bounds, results); } public void Clear() 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/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..ccb4a99 --- /dev/null +++ b/Assets/QuadNode.cs @@ -0,0 +1,12 @@ +namespace NativeQuadTree +{ + 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/QuadTreeCircleRangeQuery.cs b/Assets/QuadTreeCircleRangeQuery.cs deleted file mode 100644 index 542fd27..0000000 --- a/Assets/QuadTreeCircleRangeQuery.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 QuadTreeCircleRangeQuery - { - NativeQuadTree tree; - - UnsafeList* fastResults; - int count; - - 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) - { - 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/QuadTreeJobs.cs b/Assets/QuadTreeJobs.cs deleted file mode 100644 index d5606d0..0000000 --- a/Assets/QuadTreeJobs.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Diagnostics; -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 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"); - } - } - - /// - /// 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/QuadTreeRectRangeQuery.cs b/Assets/QuadTreeRectRangeQuery.cs deleted file mode 100644 index 0832619..0000000 --- a/Assets/QuadTreeRectRangeQuery.cs +++ /dev/null @@ -1,109 +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 QuadTreeRectRangeQuery - { - 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 From b15cdf9d10ed74740aabbb0f2c9d6f8968f05cc5 Mon Sep 17 00:00:00 2001 From: Crener Date: Sat, 28 Aug 2021 16:49:47 +0100 Subject: [PATCH 06/16] Fixed some collision calculation logic when using circle and added unit tests --- Assets/AABB2D.cs | 41 +++++++- Assets/Circle2D.cs | 27 ++---- Assets/Editor/CollisionShapeTests.cs | 94 +++++++++++++++++++ Assets/Editor/CollisionShapeTests.cs.meta | 11 +++ Assets/Editor/QuadTreeTests.cs | 1 - .../Jobs/Internal/QuadTreeCircleRangeQuery.cs | 12 +-- .../Jobs/Internal/QuadTreeRectRangeQuery.cs | 8 +- Assets/Jobs/NativeQuadTreeParallelAdd.cs | 10 +- Assets/NativeQuadTree.cs | 24 +++-- 9 files changed, 177 insertions(+), 51 deletions(-) create mode 100644 Assets/Editor/CollisionShapeTests.cs create mode 100644 Assets/Editor/CollisionShapeTests.cs.meta diff --git a/Assets/AABB2D.cs b/Assets/AABB2D.cs index 83735b9..dae08ab 100644 --- a/Assets/AABB2D.cs +++ b/Assets/AABB2D.cs @@ -1,9 +1,11 @@ using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Unity.Mathematics; namespace NativeQuadTree { - [Serializable] + [Serializable, DebuggerDisplay("Center: {Center}, Extents: {Extents}")] public struct AABB2D { public float2 Center; public float2 Extents; @@ -37,10 +39,43 @@ public bool Contains(float2 point) { } public bool Contains(AABB2D b) { - return Contains(b.Center + new float2(-b.Extents.x, -b.Extents.y)) && + 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 + new float2(b.Extents.x, b.Extents.y)); + Contains(b.Center + b.Extents); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Circle2D b) + { + float closestX = math.clamp(b.Center.x, Center.x - Extents.x, Center.x + Extents.x); + if(math.distance(closestX, b.Center.x) < b.Radious) + { + return true; + } + + float closestY = math.clamp(b.Center.y, Center.y - Extents.y, Center.y + Extents.y); + if(math.distance(closestY, b.Center.y) < b.Radious) + { + return true; + } + + return math.distance(Center, b.Center + -Extents) <= b.Radious && + math.distance(Center, b.Center + new float2(-Extents.x, Extents.y)) <= b.Radious && + math.distance(Center, b.Center + new float2(Extents.x, -Extents.y)) <= b.Radious && + math.distance(Center, b.Center + Extents.x) <= b.Radious; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Intersects(Circle2D b) + { + float2 squareEdgePoint = new float2 + { + x = math.clamp(b.Center.x, Center.x - Extents.x, Center.x + Extents.x), + y = math.clamp(b.Center.y, Center.y - Extents.y, Center.y + Extents.y) + }; + + return math.distance(squareEdgePoint, Center) <= b.Radious; } public bool Intersects(AABB2D b) diff --git a/Assets/Circle2D.cs b/Assets/Circle2D.cs index 8f5621b..2f9fbfc 100644 --- a/Assets/Circle2D.cs +++ b/Assets/Circle2D.cs @@ -8,31 +8,18 @@ public struct Circle2D { public float2 Center; public float Radious; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Contains(float2 point) - { - return math.distance(point, Center) <= Radious; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - 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 Circle2D(float2 center, float radious) + : this() + { + Center = center; + Radious = radious; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Intersects(AABB2D b) + public bool Contains(float2 point) { - float2 center = new float2() - { - x = math.clamp(Center.x, b.Center.x - b.Extents.x, b.Center.x + b.Extents.x), - y = math.clamp(Center.y, b.Center.y - b.Extents.y, b.Center.y + b.Extents.y) - }; - - return Contains(center); + return math.distance(point, Center) <= Radious; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Assets/Editor/CollisionShapeTests.cs b/Assets/Editor/CollisionShapeTests.cs new file mode 100644 index 0000000..eb158dc --- /dev/null +++ b/Assets/Editor/CollisionShapeTests.cs @@ -0,0 +1,94 @@ +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 CircleContains() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 1f); + + Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + } + + [Test] + public void CircleContains2() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 5f); + + Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + } + + [Test] + public void CircleContains3() + { + AABB2D box = new AABB2D(new float2(5f), new float2(5f)); + Circle2D testCircle = new Circle2D(5f, 8f); + + Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + } + + [Test] + public void CircleContains4() + { + AABB2D box = new AABB2D(new float2(50f), new float2(50f)); + Circle2D testCircle = new Circle2D(5f, 3f); + + Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + } +} \ 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/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 544f54b..557cc0a 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -61,7 +61,6 @@ public void InsertTriggerDivideBulk() QuadTreeDrawer.Draw(data.Value); job.QuadTree.Dispose(); - data.Dispose(); elements.Dispose(); } diff --git a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs index c5e07c7..644ab4c 100644 --- a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -43,11 +43,11 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p var contained = parentContained; if(!contained) { - if(bounds.Contains(childBounds)) + if(childBounds.Contains(bounds)) { contained = true; } - else if(!bounds.Intersects(childBounds)) + else if(!childBounds.Intersects(bounds)) { continue; } @@ -55,7 +55,6 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p var at = prevOffset + l * depthSize; - var elementCount = UnsafeUtility.ReadArrayElement(tree.lookup->Ptr, at); if(elementCount > tree.MaxLeafElements && depth < tree.MaxDepth) @@ -68,10 +67,9 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p 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>()); + 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 diff --git a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs index 4f83c97..bba7075 100644 --- a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs @@ -55,7 +55,6 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p var at = prevOffset + l * depthSize; - var elementCount = UnsafeUtility.ReadArrayElement(tree.lookup->Ptr, at); if(elementCount > tree.MaxLeafElements && depth < tree.MaxDepth) @@ -68,10 +67,9 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p 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>()); + 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 diff --git a/Assets/Jobs/NativeQuadTreeParallelAdd.cs b/Assets/Jobs/NativeQuadTreeParallelAdd.cs index 4285bb5..5cade6e 100644 --- a/Assets/Jobs/NativeQuadTreeParallelAdd.cs +++ b/Assets/Jobs/NativeQuadTreeParallelAdd.cs @@ -42,10 +42,10 @@ public static JobHandle SetupBulkAddJobChain(NativeReference> const int threadBucketSize = 100; JobHandle initHandle = init.Schedule(dependency); - JobHandle morton1Handle = mortonCreate.ScheduleBatch(quadElementArray.Length, threadBucketSize, initHandle); //JobHandle morton1Handle = mortonCreate.Schedule(initHandle); - JobHandle morton2Handle = mortonIndex.ScheduleBatch(quadElementArray.Length, threadBucketSize, morton1Handle); - //JobHandle morton2Handle = mortonIndex.Schedule(morton1Handle); + 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); @@ -69,7 +69,7 @@ public void Execute() } [BurstCompile] - private struct PrepairMortonCodesJob : IJobParallelForBatch + private struct PrepairMortonCodesJob : IJob, IJobParallelForBatch { [ReadOnly] public NativeArray> Elements; @@ -110,7 +110,7 @@ public void Execute(int startIndex, int count) } [BurstCompile] - private unsafe struct IndexMortonCodesJob : IJobParallelForBatch + private unsafe struct IndexMortonCodesJob : IJob//ParallelForBatch { public NativeArray MortonCodes; [ReadOnly] diff --git a/Assets/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index cafd176..49d7a92 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -56,7 +56,7 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m ) : this() { this.bounds = bounds; - this.m_maxDepth = maxDepth; + m_maxDepth = maxDepth; this.maxLeafElements = maxLeafElements; elementsCount = 0; @@ -148,11 +148,7 @@ internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray maxLeafElements) - { - // the allocation done in the constructor limits the amount of elements in each leaf - AssertIsTrue(false, "Quad Tree node " + atIndex + " is filled with elements, consider allocating a larger leaf node size than " + maxLeafElements); - } + AssertIsTrue(atIndex, node.count); #endif // We found a leaf, add this element to it and move to the next element @@ -168,9 +164,13 @@ internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray maxLeafElements) + { + // 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)] @@ -182,6 +182,11 @@ private NativeArray PrepairMortonCodes(NativeArray> incoming 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) { @@ -209,7 +214,7 @@ internal void PrepairMortonCodesIndex(NativeArray mortonCodes) 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); } } @@ -232,7 +237,6 @@ internal void RecursivePrepareLeaves(int prevOffset, int depth) for (int l = 0; l < 4; l++) { int at = prevOffset + l * LookupTables.DepthSizeLookup[m_maxDepth - depth+1]; - int elementCount = UnsafeUtility.ReadArrayElement(lookup->Ptr, at); if(elementCount > maxLeafElements && depth < m_maxDepth) From a1a65c5002589c034d25ef3a4f6a0b81270f11cd Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 29 Aug 2021 21:28:09 +0100 Subject: [PATCH 07/16] Fixed issues with Circle Range queries, added unit tests for correct intersection behaviour --- Assets/AABB2D.cs | 161 ++++++++++-------- Assets/Editor/CollisionShapeTests.cs | 80 ++++++++- Assets/Editor/NativeQuadTree.Editor.asmdef | 2 +- Assets/Jobs/CircleQueryJob.cs | 36 ++++ Assets/Jobs/CircleQueryJob.cs.meta | 11 ++ .../Jobs/Internal/QuadTreeCircleRangeQuery.cs | 3 +- Assets/NativeQuadTree.asmdef | 2 +- 7 files changed, 215 insertions(+), 80 deletions(-) create mode 100644 Assets/Jobs/CircleQueryJob.cs create mode 100644 Assets/Jobs/CircleQueryJob.cs.meta diff --git a/Assets/AABB2D.cs b/Assets/AABB2D.cs index dae08ab..4aeb380 100644 --- a/Assets/AABB2D.cs +++ b/Assets/AABB2D.cs @@ -1,94 +1,109 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using PlasticGui.WorkspaceWindow.Items; using Unity.Mathematics; namespace NativeQuadTree { - [Serializable, DebuggerDisplay("Center: {Center}, Extents: {Extents}")] - public struct AABB2D { - public float2 Center; - public float2 Extents; + [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 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(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; - } + public bool Contains(float2 point) + { + 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; - } + if(point[0] > Center[0] + Extents[0]) + return false; - return true; - } + if(point[1] < Center[1] - Extents[1]) + return false; - 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); - } + if(point[1] > Center[1] + Extents[1]) + return false; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Contains(Circle2D b) - { - float closestX = math.clamp(b.Center.x, Center.x - Extents.x, Center.x + Extents.x); - if(math.distance(closestX, b.Center.x) < b.Radious) - { - return true; - } - - float closestY = math.clamp(b.Center.y, Center.y - Extents.y, Center.y + Extents.y); - if(math.distance(closestY, b.Center.y) < b.Radious) - { - return true; - } + return true; + } - return math.distance(Center, b.Center + -Extents) <= b.Radious && - math.distance(Center, b.Center + new float2(-Extents.x, Extents.y)) <= b.Radious && - math.distance(Center, b.Center + new float2(Extents.x, -Extents.y)) <= b.Radious && - math.distance(Center, b.Center + Extents.x) <= b.Radious; - } + 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 Intersects(Circle2D b) - { - float2 squareEdgePoint = new float2 - { - x = math.clamp(b.Center.x, Center.x - Extents.x, Center.x + Extents.x), - y = math.clamp(b.Center.y, Center.y - Extents.y, Center.y + Extents.y) - }; + [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); - return math.distance(squareEdgePoint, Center) <= b.Radious; - } + 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; + } - 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]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Intersects(Circle2D b) + { + float2 squareEdgePoint = math.clamp(b.Center, Center - Extents, Center + Extents); + float distance = math.distance(squareEdgePoint, Center); + + if(Contains(b.Center)) + { + // inside box + float length = math.max(Extents.x, Extents.y); + return distance > b.Radious || length < b.Radious; + } + else + { + // outside box + return distance > b.Radious; + } + } + + 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[0] - b.Center[0]) < (Extents[0] + b.Extents[0])) && + (math.abs(Center[1] - b.Center[1]) < (Extents[1] + b.Extents[1])); + } + } } \ No newline at end of file diff --git a/Assets/Editor/CollisionShapeTests.cs b/Assets/Editor/CollisionShapeTests.cs index eb158dc..25838cc 100644 --- a/Assets/Editor/CollisionShapeTests.cs +++ b/Assets/Editor/CollisionShapeTests.cs @@ -62,7 +62,7 @@ public void CircleContains() AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 1f); - Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + Assert.IsTrue(box.Contains(testCircle), "fully enclosed inside square"); } [Test] @@ -71,7 +71,7 @@ public void CircleContains2() AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 5f); - Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + Assert.IsTrue(box.Contains(testCircle), "fully enclosed inside square"); } [Test] @@ -80,7 +80,16 @@ public void CircleContains3() AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 8f); - Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + Assert.IsFalse(box.Contains(testCircle), "the circle is outside the bounds of the square so it isn't fully contained"); + } + + [Test] + public void CircleContains3V2() + { + 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] @@ -89,6 +98,69 @@ public void CircleContains4() AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(5f, 3f); - Assert.IsTrue(box.Contains(testCircle), "partial overlap without either square containing a corner"); + Assert.IsTrue(box.Contains(testCircle), "fully contained"); + } + + [Test] + public void CircleContains5() + { + 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 CircleContains6() + { + 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 CircleIntersect() + { + 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 CircleIntersect2() + { + 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 CircleIntersect3() + { + 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 CircleIntersect4() + { + 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 CircleIntersect5() + { + 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"); } } \ No newline at end of file diff --git a/Assets/Editor/NativeQuadTree.Editor.asmdef b/Assets/Editor/NativeQuadTree.Editor.asmdef index 39b122a..95821e6 100644 --- a/Assets/Editor/NativeQuadTree.Editor.asmdef +++ b/Assets/Editor/NativeQuadTree.Editor.asmdef @@ -1,6 +1,6 @@ { "name": "NativeQuadTree.Editor", - "rootNamespace": "", + "rootNamespace": "NativeQuadTree.Editor", "references": [ "GUID:4259b4454b3b86a4790bd00dd10d9797", "GUID:d8b63aba1907145bea998dd612889d6b", 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/QuadTreeCircleRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs index 644ab4c..cad7ce0 100644 --- a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -5,10 +5,11 @@ namespace NativeQuadTree.Jobs.Internal { - internal unsafe struct QuadTreeCircleRangeQuery where T : unmanaged + public unsafe struct QuadTreeCircleRangeQuery where T : unmanaged { private NativeQuadTree tree; + [NativeDisableUnsafePtrRestrictionAttribute] private UnsafeList* fastResults; private int count; diff --git a/Assets/NativeQuadTree.asmdef b/Assets/NativeQuadTree.asmdef index 1066e2b..247e036 100644 --- a/Assets/NativeQuadTree.asmdef +++ b/Assets/NativeQuadTree.asmdef @@ -1,6 +1,6 @@ { "name": "NativeQuadTree", - "rootNamespace": "", + "rootNamespace": "NativeQuadTree", "references": [ "GUID:2665a8d13d1b3f18800f46e256720795", "GUID:e0cd26848372d4e5c891c569017e11f1", From 56a4a30f75a42f3fc90a976b0ff4f64b61f73da2 Mon Sep 17 00:00:00 2001 From: Crener Date: Mon, 30 Aug 2021 18:39:36 +0100 Subject: [PATCH 08/16] Created demo scene for debugging results, Fixed inacurate circle results, added more tests, updated solution to 2021.2 --- Assets/AABB2D.cs | 22 +- Assets/Circle2D.cs | 34 +- Assets/Editor/CollisionShapeTests.cs | 150 ++++- Assets/Example.meta | 8 + Assets/Example/NativeQuadTree.Example.asmdef | 19 + .../NativeQuadTree.Example.asmdef.meta | 7 + Assets/Example/NativeQuadTreeCreator.cs | 121 ++++ Assets/Example/NativeQuadTreeCreator.cs.meta | 11 + Assets/Example/NativeQueryCircle.cs | 67 +++ Assets/Example/NativeQueryCircle.cs.meta | 11 + Assets/Example/NativeQueryRect.cs | 83 +++ Assets/Example/NativeQueryRect.cs.meta | 3 + Assets/Example/Test Scene.unity | 353 +++++++++++ Assets/Example/Test Scene.unity.meta | 7 + .../Jobs/Internal/QuadTreeCircleRangeQuery.cs | 6 +- .../Jobs/Internal/QuadTreeRectRangeQuery.cs | 3 +- Assets/Jobs/RectQueryJob.cs | 36 ++ Assets/Jobs/RectQueryJob.cs.meta | 3 + Packages/manifest.json | 21 +- Packages/packages-lock.json | 566 ++++++++++++++++++ ProjectSettings/BurstAotSettings_Android.json | 13 + .../BurstAotSettings_StandaloneWindows.json | 16 + ProjectSettings/CommonBurstAotSettings.json | 6 + ProjectSettings/PackageManagerSettings.asset | 45 ++ ProjectSettings/ProjectSettings.asset | 122 ++-- ProjectSettings/ProjectVersion.txt | 4 +- ProjectSettings/VersionControlSettings.asset | 8 + ProjectSettings/XRPackageSettings.asset | 5 + UserSettings/EditorUserSettings.asset | 30 + UserSettings/Search.settings | 1 + 30 files changed, 1683 insertions(+), 98 deletions(-) create mode 100644 Assets/Example.meta create mode 100644 Assets/Example/NativeQuadTree.Example.asmdef create mode 100644 Assets/Example/NativeQuadTree.Example.asmdef.meta create mode 100644 Assets/Example/NativeQuadTreeCreator.cs create mode 100644 Assets/Example/NativeQuadTreeCreator.cs.meta create mode 100644 Assets/Example/NativeQueryCircle.cs create mode 100644 Assets/Example/NativeQueryCircle.cs.meta create mode 100644 Assets/Example/NativeQueryRect.cs create mode 100644 Assets/Example/NativeQueryRect.cs.meta create mode 100644 Assets/Example/Test Scene.unity create mode 100644 Assets/Example/Test Scene.unity.meta create mode 100644 Assets/Jobs/RectQueryJob.cs create mode 100644 Assets/Jobs/RectQueryJob.cs.meta create mode 100644 Packages/packages-lock.json create mode 100644 ProjectSettings/BurstAotSettings_Android.json create mode 100644 ProjectSettings/BurstAotSettings_StandaloneWindows.json create mode 100644 ProjectSettings/CommonBurstAotSettings.json create mode 100644 ProjectSettings/PackageManagerSettings.asset create mode 100644 ProjectSettings/VersionControlSettings.asset create mode 100644 ProjectSettings/XRPackageSettings.asset create mode 100644 UserSettings/EditorUserSettings.asset create mode 100644 UserSettings/Search.settings diff --git a/Assets/AABB2D.cs b/Assets/AABB2D.cs index 4aeb380..8f3c682 100644 --- a/Assets/AABB2D.cs +++ b/Assets/AABB2D.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using PlasticGui.WorkspaceWindow.Items; using Unity.Mathematics; +using UnityEngine; namespace NativeQuadTree { @@ -21,6 +22,12 @@ 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) { @@ -77,20 +84,7 @@ public bool Contains(Circle2D b) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Intersects(Circle2D b) { - float2 squareEdgePoint = math.clamp(b.Center, Center - Extents, Center + Extents); - float distance = math.distance(squareEdgePoint, Center); - - if(Contains(b.Center)) - { - // inside box - float length = math.max(Extents.x, Extents.y); - return distance > b.Radious || length < b.Radious; - } - else - { - // outside box - return distance > b.Radious; - } + return Circle2D.Intersects(this, b); } public bool Intersects(AABB2D b) diff --git a/Assets/Circle2D.cs b/Assets/Circle2D.cs index 2f9fbfc..c712fbe 100644 --- a/Assets/Circle2D.cs +++ b/Assets/Circle2D.cs @@ -23,9 +23,39 @@ public bool Contains(float2 point) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Intersects(Circle2D b) + public bool Contains(AABB2D b) { - return math.distance(Center, b.Center) <= Radious + b.Radious; + // 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, a.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/Editor/CollisionShapeTests.cs b/Assets/Editor/CollisionShapeTests.cs index 25838cc..539ce52 100644 --- a/Assets/Editor/CollisionShapeTests.cs +++ b/Assets/Editor/CollisionShapeTests.cs @@ -57,7 +57,7 @@ public void SquareEdgePartialIntersect2() } [Test] - public void CircleContains() + public void SquareCircleContains() { AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 1f); @@ -66,7 +66,7 @@ public void CircleContains() } [Test] - public void CircleContains2() + public void SquareCircleContains2() { AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 5f); @@ -75,7 +75,7 @@ public void CircleContains2() } [Test] - public void CircleContains3() + public void SquareCircleContains3() { AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 8f); @@ -84,7 +84,7 @@ public void CircleContains3() } [Test] - public void CircleContains3V2() + public void SquareCircleContains3V2() { AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(6f, 8f); @@ -93,7 +93,7 @@ public void CircleContains3V2() } [Test] - public void CircleContains4() + public void SquareCircleContains4() { AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(5f, 3f); @@ -102,7 +102,7 @@ public void CircleContains4() } [Test] - public void CircleContains5() + public void SquareCircleContains5() { AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(-5f, 3f); @@ -111,7 +111,7 @@ public void CircleContains5() } [Test] - public void CircleContains6() + public void SquareCircleContains6() { AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(3.1f, 3f); @@ -120,7 +120,7 @@ public void CircleContains6() } [Test] - public void CircleIntersect() + public void SquareCircleIntersect() { AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(new float2(1f, 50f), 3f); @@ -129,7 +129,7 @@ public void CircleIntersect() } [Test] - public void CircleIntersect2() + public void SquareCircleIntersect2() { AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(new float2(-1f, 50f), 3f); @@ -138,7 +138,7 @@ public void CircleIntersect2() } [Test] - public void CircleIntersect3() + public void SquareCircleIntersect3() { AABB2D box = new AABB2D(new float2(5f), new float2(5f)); Circle2D testCircle = new Circle2D(5f, 8f); @@ -147,7 +147,7 @@ public void CircleIntersect3() } [Test] - public void CircleIntersect4() + public void SquareCircleIntersect4() { AABB2D box = new AABB2D(new float2(50f), new float2(50f)); Circle2D testCircle = new Circle2D(new float2(50f, 1f), 3f); @@ -156,11 +156,137 @@ public void CircleIntersect4() } [Test] - public void CircleIntersect5() + 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/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..d2c2070 --- /dev/null +++ b/Assets/Example/NativeQuadTreeCreator.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using NativeQuadTree; +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 + { + float x = Random.Range(rect.Center.x - rect.Extents.x, rect.Center.x + rect.Extents.x); + float y = Random.Range(rect.Center.y - rect.Extents.y, rect.Center.y + rect.Extents.y); + float2 testPos = new float2(x, y); + + if(rect.Contains(testPos)) + { + QuadElement element = new QuadElement() + { + Pos = testPos + }; + Positions.Add(element); + } + } + 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"); + + 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.x * .5f; + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x - half, rect.Center.y + half), half), division - 1); + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x + half, rect.Center.y + half), half), division - 1); + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x - half, rect.Center.y - half), half), division - 1); + DrawRectSubdivide(new AABB2D(new float2(rect.Center.x + half, rect.Center.y - half), 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..e82ec73 --- /dev/null +++ b/Assets/Example/NativeQueryCircle.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NativeQuadTree; +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>(200, 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..62771fa --- /dev/null +++ b/Assets/Example/NativeQueryRect.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NativeQuadTree; +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 circle = 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>(200, Allocator.TempJob); + + RectQueryJob query = new RectQueryJob(circle, 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(xMin, 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..89e7434 --- /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.8, y: -2.9} + m_SizeDelta: {x: 20.9233, y: 20.9233} + 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: 77.02 + 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: 10 +--- !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: -31.5, y: 31.87, 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: 4 + LeafUnits: 200 + 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: 100, y: 100} + 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/Jobs/Internal/QuadTreeCircleRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs index cad7ce0..85ff045 100644 --- a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -9,7 +9,7 @@ public unsafe struct QuadTreeCircleRangeQuery where T : unmanaged { private NativeQuadTree tree; - [NativeDisableUnsafePtrRestrictionAttribute] + [NativeDisableUnsafePtrRestriction] private UnsafeList* fastResults; private int count; @@ -44,11 +44,11 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p var contained = parentContained; if(!contained) { - if(childBounds.Contains(bounds)) + if(bounds.Contains(childBounds)) { contained = true; } - else if(!childBounds.Intersects(bounds)) + else if(!bounds.Intersects(childBounds)) { continue; } diff --git a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs index bba7075..08ac2fd 100644 --- a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs @@ -5,10 +5,11 @@ namespace NativeQuadTree.Jobs.Internal { - internal unsafe struct QuadTreeRectRangeQuery where T : unmanaged + public unsafe struct QuadTreeRectRangeQuery where T : unmanaged { private NativeQuadTree tree; + [NativeDisableUnsafePtrRestriction] private UnsafeList* fastResults; private int count; 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/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/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 From 1f4b0eefcf7c0da8bc3e4ad7fcddee7ecbecd3eb Mon Sep 17 00:00:00 2001 From: Crener Date: Mon, 30 Aug 2021 19:44:17 +0100 Subject: [PATCH 09/16] Found and fixed edge case where center of circle was outside the tested box --- Assets/Circle2D.cs | 5 ++- Assets/Example/NativeQuadTreeCreator.cs | 10 ++--- Assets/Example/NativeQueryCircle.cs | 3 +- Assets/Example/NativeQueryRect.cs | 7 ++-- Assets/Example/Test Scene.unity | 14 +++---- Assets/Helpers/ArraySizeHelpers.cs | 41 +++++++++++++++++++ Assets/Helpers/ArraySizeHelpers.cs.meta | 3 ++ .../Jobs/Internal/QuadTreeCircleRangeQuery.cs | 16 +------- .../Jobs/Internal/QuadTreeRectRangeQuery.cs | 16 +------- Assets/Jobs/Internal/RangeQueryHelpers.cs | 22 ++++++++++ .../Jobs/Internal/RangeQueryHelpers.cs.meta | 3 ++ 11 files changed, 92 insertions(+), 48 deletions(-) create mode 100644 Assets/Helpers/ArraySizeHelpers.cs create mode 100644 Assets/Helpers/ArraySizeHelpers.cs.meta create mode 100644 Assets/Jobs/Internal/RangeQueryHelpers.cs create mode 100644 Assets/Jobs/Internal/RangeQueryHelpers.cs.meta diff --git a/Assets/Circle2D.cs b/Assets/Circle2D.cs index c712fbe..99e6fd2 100644 --- a/Assets/Circle2D.cs +++ b/Assets/Circle2D.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.CompilerServices; using NativeQuadTree; using Unity.Mathematics; @@ -42,7 +43,7 @@ public bool Intersects(AABB2D a) 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, a.Center); + float distance = math.distance(squareEdgePoint, b.Center); if(a.Contains(b.Center)) { @@ -54,7 +55,7 @@ internal static bool Intersects(AABB2D a, Circle2D b) else { // outside box - return distance > b.Radious; + return distance < b.Radious; } } } diff --git a/Assets/Example/NativeQuadTreeCreator.cs b/Assets/Example/NativeQuadTreeCreator.cs index d2c2070..eef8475 100644 --- a/Assets/Example/NativeQuadTreeCreator.cs +++ b/Assets/Example/NativeQuadTreeCreator.cs @@ -108,11 +108,11 @@ private void DrawRectSubdivide(AABB2D rect, int division) { if(division > 0) { - var half = rect.Extents.x * .5f; - DrawRectSubdivide(new AABB2D(new float2(rect.Center.x - half, rect.Center.y + half), half), division - 1); - DrawRectSubdivide(new AABB2D(new float2(rect.Center.x + half, rect.Center.y + half), half), division - 1); - DrawRectSubdivide(new AABB2D(new float2(rect.Center.x - half, rect.Center.y - half), half), division - 1); - DrawRectSubdivide(new AABB2D(new float2(rect.Center.x + half, rect.Center.y - half), half), division - 1); + 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); diff --git a/Assets/Example/NativeQueryCircle.cs b/Assets/Example/NativeQueryCircle.cs index e82ec73..fbee8ac 100644 --- a/Assets/Example/NativeQueryCircle.cs +++ b/Assets/Example/NativeQueryCircle.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using NativeQuadTree; +using NativeQuadTree.Helpers; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; @@ -24,7 +25,7 @@ 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>(200, Allocator.TempJob); + NativeList> results = new NativeList>(Tree.tree.EstimateResultSize(circle), Allocator.TempJob); CircleQueryJob query = new CircleQueryJob(circle, treeRef, results); query.Schedule().Complete(); diff --git a/Assets/Example/NativeQueryRect.cs b/Assets/Example/NativeQueryRect.cs index 62771fa..77a6f02 100644 --- a/Assets/Example/NativeQueryRect.cs +++ b/Assets/Example/NativeQueryRect.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using NativeQuadTree; +using NativeQuadTree.Helpers; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; @@ -22,11 +23,11 @@ void Start() void Update() { - AABB2D circle = new AABB2D(new float2(trans.position.x, trans.position.y), (trans.rect.max - trans.rect.min) / 2f); + 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>(200, Allocator.TempJob); + NativeList> results = new NativeList>(Tree.tree.EstimateResultSize(box), Allocator.TempJob); - RectQueryJob query = new RectQueryJob(circle, treeRef, results); + RectQueryJob query = new RectQueryJob(box, treeRef, results); query.Schedule().Complete(); Results = results.ToArray(); diff --git a/Assets/Example/Test Scene.unity b/Assets/Example/Test Scene.unity index 89e7434..96ccfff 100644 --- a/Assets/Example/Test Scene.unity +++ b/Assets/Example/Test Scene.unity @@ -169,8 +169,8 @@ RectTransform: 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.8, y: -2.9} - m_SizeDelta: {x: 20.9233, y: 20.9233} + 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: @@ -225,7 +225,7 @@ Camera: far clip plane: 1000 field of view: 60 orthographic: 1 - orthographic size: 77.02 + orthographic size: 33.449852 m_Depth: -1 m_CullingMask: serializedVersion: 2 @@ -285,7 +285,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Tree: {fileID: 1933793337} - Radious: 10 + Radious: 8.71 --- !u!4 &1739482048 Transform: m_ObjectHideFlags: 0 @@ -294,7 +294,7 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1739482046} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -31.5, y: 31.87, z: -6.3946342} + 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} @@ -329,7 +329,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f3a2756cc3eca64d9742d7ae481dca7, type: 3} m_Name: m_EditorClassIdentifier: - Depth: 4 + Depth: 3 LeafUnits: 200 PositionCount: 2000 --- !u!224 &1933793338 @@ -349,5 +349,5 @@ RectTransform: 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: 100, y: 100} + m_SizeDelta: {x: 50, y: 50} m_Pivot: {x: 0.5, y: 0.5} 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/Jobs/Internal/QuadTreeCircleRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs index 85ff045..2f3f2a9 100644 --- a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -39,7 +39,7 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p var depthSize = LookupTables.DepthSizeLookup[tree.MaxDepth - depth + 1]; for (int l = 0; l < 4; l++) { - var childBounds = GetChildBounds(parentBounds, l); + var childBounds = RangeQueryHelpers.GetChildBounds(parentBounds, l); var contained = parentContained; if(!contained) @@ -87,19 +87,5 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p } } } - - private 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/QuadTreeRectRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs index 08ac2fd..beb375e 100644 --- a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs @@ -39,7 +39,7 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p var depthSize = LookupTables.DepthSizeLookup[tree.MaxDepth - depth + 1]; for (int l = 0; l < 4; l++) { - var childBounds = GetChildBounds(parentBounds, l); + var childBounds = RangeQueryHelpers.GetChildBounds(parentBounds, l); var contained = parentContained; if(!contained) @@ -87,19 +87,5 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p } } } - - private 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(); - } - } } } 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 From 5bd00a211fa662c51cfdc34f0f8f91d73d8d153a Mon Sep 17 00:00:00 2001 From: Crener Date: Mon, 30 Aug 2021 21:06:45 +0100 Subject: [PATCH 10/16] Added example image to readme --- Demo Scene.PNG | Bin 0 -> 42546 bytes README.md | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 Demo Scene.PNG diff --git a/Demo Scene.PNG b/Demo Scene.PNG new file mode 100644 index 0000000000000000000000000000000000000000..a0eecbdfda56f923283e4cd710d3d740c852a249 GIT binary patch literal 42546 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

1nB9M36>D`DMo6RAn=Www6( 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ðr&|7O-eTsFef#D+aUtTf!aPmwn41vFwEKHek3y4rLq31oGq9S4s- zH_f>hl=PE(xAj&;FXf}VT>(2E2#tKc>egx*aK>jY_2k)b*2MW{q35jx#@kL7Rshg5{PCT7BCIcCCqZ3r%R}KmrSq-k z@9IGNbuHipfNK0ZKZoLXmAnEXNv1^l**Z0(2wad*0x3ndbEjY&>fmj9pJ{e)s%Ge2 zRDzazLfiq~{><+0j*+g9LSEG6iO#qt5F22f2KM&Rx%`qC1L#b2=Ntf`iXl8pPv&Mr zXxM}<)BorBZ%Fp|{1*_o$o)zd2AH0|6zwf7_(uU*Tu)3Fid#)DK(Z}9g3wz2hL#Fg zhz|&epEWn;IuABLuGPrR3529p*>H5wYjZ`hEw)VC-e3kaxys`J!U*Gr^v`xASd`;} z^)3L0q4_)3CGSu3gOf=1acu90wCAZr$H8XP#wmbETS1%ozs&^_4_(_>#E|t zgx1ji*gQs`9&pifzX_XY0%d$1xHHw8VfkjMxVJIxh7uGPr@^TbYEVq}eLELr5^%r} z{L$bO@=Tu{JQw21KNi04}te>Nw7|57>qDs~CS#fEC+)WvxwxDOlLY0V?d-g#gdc(=bQ64w$HU zPWr9+&u$p~eyI|W2bG@U?xn5nJ-~Y!E%-e5tfM2XKq-ah(u>dQ4VGD|tQse_a|zI_ zlBn3nw#*j=Ce2xqlEevx<#5q!FvGdo*XY06+i!Atbtpz6DrUZW&FI zziZ=a4BsNJlnM7%xNCw~TTW4^QfQbhaeV~kKVKw$z*CUZe`saoJRd)PoSB~VSX$cZ z-Ku_DXUwJ=Y-oe339ZT9W`;?kX7(%5ySk77uD)tib(gsL%2`|dF{ z?@S)_X-XjN(5sfLW>LS2raK zn|gBH6&Qsi9Ioiy>~HS!pbi)`Dt4c;%bR6#X=uhS41l|w1T0nrkVk2 zgndOZQXpu4gOVO=J)j7G8}TAF4#~p;!=7*=_a@w*`@I4lBXE}C+W!tRR>j*d+UAzC z`+eSLd&hq@=YtkZ`(toQUY%FD0DYc>^~MLv6Jc(ljInmDS>b1V*CbBqzr+7VPaFDa zl`)7Yn0QYqo4fOQpcFlDO3KfGo)qf*C!ac;jOs3dZB}@g7uY-0fq&6~<>#2N!=g}- z^6dF~&&Uy1nr|P30)}u!XyS!w*z@?%bW5N-g_;6uMmP+y5w~h`lIERo2V4_sL|JvZ z)xMG+VYI#X0lMoEGLR4NBs;-{GLd#2;eaAEDc8&{chj9^Kj(<-g0*5JOFb?u+PoeY*m54}Tj92(H}@6$N&TF`mpw$_6eXXC{spt%_6pouE0kT~FI;YE7J~pS7j|RXJmHij-Xr656RckRzn36?kU);@Qow zl3)`^b7SAFg|RMKPj59A7QjwJF_kITYCS{+5`vuU{kM`x&qJRUMMPO;P?}>q1et(v7bxBI|35cFZ2E(s7V9tW;k?CW3O5i^ zs3gl(x7p_}2KiYN4rbV`qUk(hiql=%iGO$|>Qo5g+ZE02_f{2-s7o{I7qY8nfSz83 zs-P=pU}P_|qWAd_b%NQli12TXPGwEqtZ7NLp2(4Svrh;t3_HLRodl(C5v!BvRHAta zdR}uN;*qX!g{RrX8dxt+O$Z>;vvnS(0cCd6Q)}qA6aF-4i7?HGqU!C{>`+e;mH>u0 z(!Y;g$}I}|=#{gOjP%jR$NjR$WmE3k-O<|w>Pi+4J5@%L!cJi?{> z^tD7qN-4I)l=OGpuyOSB)m9dUD(mSq-B^6dg8lVb&*#v?mlyGMjc9Grz9OF{Zd9H2 zvvKE4?)CI6TX1ubuEu~UoNvF|X0bm)n)cBhq45PEd_~6SHER^?5P%%+p~UY0sXNpW z_;7kT@Vn`wUU#pXLECgD1~3RBa14o3FfXRpc9V7yK+j;@2%!yibCi-4<|)3{MGltv zAs5P_PcerCV`Ne~G_4Y{GNJ+5mweeVn17R+^IOdIxZIW?UF19{y&=i0XpCPkI`%+@ zJ=sDR@7W)I3^NmWRM(CjjtdhY<;acAX2_{bqy+g+aHJ|9^>-T@DR%92b_lm!k>yj zswlAhP&$NjVu^gv(_{LPSbR}?y>p8|iWEZT-p+$>%$p^iCJIh-AUHi85$N!Db(ftE z4_(udhO#7HeY=X7Xr9n-*jSFRP>o89VMtR?$^R^`f@^q^t^WyEMRC%iHTwoRO++vw zR477yNy0Vqv2O735YxM-?X57zRcM)&8g1wbuhyWvwreHO#d^#*&R(~vCAsI4J~=de zTO0F|)2Y<;HS}Oso>}TYPoWlo6u)rPYRcIHh=M91B(&*Bk?5^_(A>ft^DJ0%5R^jU zv;K24I9Wr7dC=ius5AbXPHq-tFBfI#T_C%nq{qqal&=G&XIT>ZKC^p0!ec1a6mYfC z>ZgW9gXfz~_}#9D4=?2iu+uBDmpMz_r0nh!ZLM!VUj@s{lN2v>SWk#NJKZ`DGwNBF zSt+TYS-m$ z%Ba|i@^#r)pDG<2@w^8AVyXQtVm;C*$hSe1#A{=|+$W8!KM+TQuC@??1x*^ehD3|E zZyG=ca)la(cUm84G92aS)$1y)!_y7D=Iv+BDU5yF{PJ%zkx0@CZ19xrHFLmP7l2+=fVEeipZ>L65t^7zrg`9557e z>qX47#z8dm1O5j50nqMj(5DNWm%aiBf;uRU2)oFq7xh4^GQOo0$HjS+*ND;ae0tuJDmT`eoD#Gp+m{NR(d$s4c$5y4@NZ5;5R}_ zKT6=L&)v>iyN+;hiZY@b)_fRQ%+`!Nj$b<-JIgFCEZ6~Oz8P-iZ>y9JW>uFw=xPot zTmI9*5MiByxui+eK3?p61uT@B)e6FkvMYzc_eFM!QPUgyl-s~Za3j!xE+oK71f<1- zxwM|95CS0qiVR{J!5d>X1+?K5W$DbOo4{gxdU1oH&;I{MAx9;aE>>{Xn>#4_A`{Fr z6`L^4er{eCpsYqm7~wSxt0-Teq#frn@AuextSLQs;W&kC3g{dj<+hkIKh&EsM2zhn zAvR@=lWwhBe9q+GU)VarIz?9{AOuVaRAeNTz+nIyr{^Yz_*xe7l~->6)K|d`0>4z> zUv{d8JGNriVDrTcpuXL@`gF8`brs^4gg{D=q_cB7tRqqsQJdgm#-9v#DeWKxafVyd zI-hLk*(}AowpJNlg;dlMw|bA%4}$d?#fp4 zh%1Y}F~zLTfmm1$D1mTl%tsnnNDSVo(xag8MC&6NSc>EF zBB3gAD&)67w8pbe-sEG5*&;)U>FlOwE=(V=SE!J6A-m~SW3tC9pQ3oIs;=Yr7AKHU zeYc6+FAFD#7cbfU-60a-8}0_}e%3%TAZm4~(!{$C3$W zdizq2;m{;?am``buO}3aS$N$x3${BTJn%6>@^AuqC2?Eb{j5mSF~e44j3Q!e^1pUx zz-&5-ij#hdo7%;xEIqypzS7ePBc{Z>{xMuj`0V{bS+C+(0MpQ7Hm8;;h20P+<#t@) zOHyW#PS_3rf#w76(cO{|v+BH#U)UPY!<`SRhc3R=g-)rY|Jl>Kjs3ic0RzjWeoW{! zeUJ9Fjf1w-*e(Ht_qn?a+9GWn<5^yla-Y)WgSZ6!uD@>`<}4lwRl3dANJ&`Otu{Bm zdi$p!$=S^D#FO-&1MVu-rLNonJ7TC2etzWK;M6fn^LSe_3PAp!#%hZ+$eggg2O(&| zl2-Z;|^uDM*uP!PgjHVjcsp&1F`C zOK73Ir(2qJLqCQI=>$5&a9)Er0#BM(G)ksi0pOB4MPRdpm#$d_@HhaOJ4?CjpD%@u z&|)UTa}%>gj-7IXdk-R1M!R2SB^Fg4r&TG`U3hmlf%8_C@=b?`w$|a<-R43M6ZhHM z!%=#KRmDYwUMWM{l!UT#Zei`K2Y!N9nQlq@ZM~zx&+R);13UQ7~$rA^(?Ay%e+le(4U z2r#8Ur-20q)VVK)YKu3`pNL^BCcw}+_bMmckQ4419wQ~s%Hy@9*-AJ)Qbe7&*FmaC z;*MMZ0XXB^AnPb8!y4<5lSR|6UqBkVz~$*4DA4IA5Kect1Ol#L*Q+40ViUNqR<*l? z67Q7$ZcTYGTRwhc(Y+0l-CXu1y6@v*O~86r*ZM5CIBz4g-T9wL;)GkGUJS}Gh&HX$ z0x7|wM^bIhky%zz(nv0U`gPQon}96Aq9k1jJ@i>(b2WmE&ax&>8B9;oq7 z+Ty*EK2yND&pZ{_4t?U!1FiOD>$Yfz<@?TQS-qIPV8MQrmMIAr-AvQy7;RXB4Mc%s zlI=hK2W|Rc7yA`hPM8TGfkz9n#yg;^1>L8mXP5X<=Fa&9lNzGvYDQSOIXxtBEH(X? z*j2!#8YYK6H4<&C%8p^##zRwqASyOXf+%KQDNhN{fX?vMN~l$!aeCgQmef3{q5!7H znY1Bdo1TO^PXpXbhWK?4k12NGNfkIvwvh071%K`E02&TW?!*E|`~PhxJlroo7+%CV z&B@z zqo})#?3eWoD-$v{MK*? z;s#gpW4mg*X3H%^+V|3C;BVePNPo|@q<$hRKE|+t~8sa@xZl9S5;j{FSMwN9l^lrON#Ft7 z!`BC0H7R63#hF;K9c=NyD3F?fa%uidMsRPEzC^myR+%MxxY0^67mw!OtkGdQsj}dU z*miljR9otDt^BLH>lM4%v&&4Q(;(f~Wg^i7gp~YywP!6(v*O$+KC4rV9VNN)5RUhkfegwC?C>&naLgdI#pRYoSdn$&Z z>rsaP0L^+`KUAQk)%AM5Z*^%4b^CfO_#VDTPy005f5VsUX5xg+Dg_s;URmvPesXAz zcO!u>0nxe#gXj_wv5neDR~4JY5HjJ)GqAW z1!xO6obE{Rk*b0?1}edn;%@|1v+4r0 zYckB39r7k@{c0~~S8;#Xrt%hFwxNb?E?$UY7C@xd6>~G%iir;aGIJ5<_%(XaT2sox$!-d z#NQJ^lZ)H}jWFDo6|k8DDt_>TId<=TzVH8XVz-@PXY4TCMPriqH=vibXpkf{1CuLs zvuqs=BfyjBI;Vdg?c%JqXz{-Av(L=P2U9@VW|@GEkjE~`3 zfE{hSw`w&$v)*k+oIpCb51%A4nYyP}0nUQv& zqmtM0C-loTt0)mK*KK|S5~y|4KM+}wRU0$>ei4e?-d>%4@x z9-yIE92%9|{shVpuVd^2XaF4mxht(a)})<@W^UWuqD$DX8VLc9vzf2Cg#MwAA#xf_pVV?I`I+ZA_3<9>pNw6$ z`b13KP9PZ1ms}z`&0w;|mH)dAe{SmrcfM$h$O zbW2nGQ)R@rkY*ja(vS!pky6{55S)AAfWi@>cuGeR0vVe4hj^mHjJ0Ef=WB?9jzY$F z?h|(ju#GgH>&^)lLY372lsq^!*;JOxDyd(f1-DYwhxW>qX$ScZHio-tJX@jQ)WBc0 z?uiPJAQTvFn$F%I4^;@bctC{5iVW!~?zN_KkBZw4LV-_HPZfnv$2#OS5NO7Q&UF#- zd)To_(AM%4lP>n-{ z?TSY_GUo1S-DRA@?2Mp7pYt)V(R!6eY~J@WT$*Za)f8f53W8z3uOuSj5nihjRQw5u zlN)?f_a9; zO%cMT##dSCw_f3X4v_H{tWDxYwKr`?41!znfdK``xL#vt=4z8dOUI=CW%e{sqE~hK zqMow1U$EZsoa37S6D7k2y1>!ThE!lEs*Csd*k6G|FNO658o8dXaDzvy*mJ#%iyxB*C)xgpN3S~LR zrM24%db>p#U5HEAebCDljJ1pvJw={ucizB8-fd*wKwR#=(*^v-G=?U5f>Ekjn8vT0@7e9@f}X|`M`Jszt`4+JsXa1ZcM|~eY49R z*SI^F@9+&9ZkbOw9JUj&C5k~Q&5okZV`uU1;bQ$3@65Lo3M~e5L`v!cEj_#N@;z~f zs{Rtv|0%J(X6H}HJn(nfquJ#3Y1U(J`D@~hySfcQkp_?9UUlBUwOS=n#&skB^XQ8g6w${$!MX@=TjTqOn% z9eT4TbQ^*I0mZ`J=gsCl5n$vS3RH-OVNERbw!6R@o#IkPerg`=5BBvr z7<39aKbs=UR=-6p`SB!s-PgLdm!TQFNb%@P!&|WTg;tlJvI>bznk!orUyPvb$Ikr0 zw*0UZ*PE*6cbF=>l6GXxX`m&CP5`=7^`A9xD&*lJ9&pMGh(ulP;Yzv{>Z#icR0RqF z@85l2rGGvF>dDdGH^bLsuMWsscG)H=Zo&rnWKp*o`!q_+qzs$*G|IB`Z6XUAGL%KR zQo1WDx2ap9OC80(laFaT_{@FEBYw$jUu_hX>bvXFW_TXINw_)vr z0!3NC8i-u`@qK|`w&eDmMkwy`g+U6gq0iKx}uS(s|a@a~auP1T=?@v!gZU=rm zxFa(|nB{Hq32~dkaGXfBWqHxh1utD~CslXpdA}UiA{~;+g#f2Q<8CQG@-Y5k7fz%~ z5Q9dTbGiPMw*;&nW0m`b#YJY8Z&T-Ed~G{VRN`*{+)f!j<{N-%0FXW!hX`+q=`3r*d9ailO>UL4%K9 zdi~{mQFTlc4*lcbP;zD*2ia8h*RnV8Rtbq2-xt}1ZXK_){xKe@l|jEu zvJ@<|aS&_Bl%x5$5*xf(87a_GCA*}L>_95PKcHUOx;Z05p_f!3tfWXUgF=KCWTYOs z_jO8kF<(!aL;0*qpO>i*fyQ*Eh;?;9Z4m~tSHA-KgE6gWj+{Y?Fyp_gA+FHW1+&*+ zs_9fx4{S2t_U99Z07kA62@`RNRz?g)RexQjx@4yADY3^C|zKMG+6J+OBOv5@<~ zPaDj)Y@0Oe-k2a?rtM3(D0^+*f9u>JOLF|{^4*=Z!K>Vx0wmwlqo4nMdkg8~aH1Qmnhedv6h) zN0!qL%;BC_*yo`mBeE*%x@HI`*?f~kmcEx03LK`NzB~qV`uQWMI}O!lXjTh^*@z?akpno9jpu3u089waQZz+948h`%e>0 zbk8$t^fA0m5i>&yB-Vev-U`v_2v|fO&k%8Sx55x(cHJ|P ztQ%Bop8Pj=B24D(x4xW^2~Vl_QVz>xm)!G4y1%*QwuXjV!Ic={`roZ+RB-p*f_@SZ zp5wd6>$Cf=a-m`M7;&d%;?QbKgLjQCFUAHe=N2LU>0f?$T@8abLX3TMXgxf59_r?k z30>_0U&G4v$)tw!h(RpkoGrR1XB>AKB!b%ZsFlo?c|vn##88+SOoZhBOSm4>h=0-3 zzPR)4ep;B{wG0ZF0M-W_LXr6oWaty-oMu@}+so{`q8@i3y$MIud3SnKxiWr|Y~^^- zNMoX(Ylk|_h9krNK$>)>7cQfTAqIw|tojfj2sn3Hc0dN+*<^qoxC3Ue^pPPEDh!FV z`_`@|g?=@||IfPgCb@BOxOUd{F+YevJ2e_ApHd-J5Uv4p;qY35vG9yVyfus~fG3AM z1q2#ifl&lgd(<45Rf0_8#6o0l=RtNdN@l4qzUU08m~qq1U{1A+VVgpv8PC@Ekq$C! zCwg1gAJZObQGobw4GI^?Q(wQ;J{gzQ>9~Fe0Z>53v3tzz<4V{XFgyUgwwNrQ&+}gQ zr+N}#`}Pl_sjK_H$YrI6^zSPivr!tWR>}}3Pbv@$G8A)fC?C*jlaJM=SF$u<^=~?3%*lS)%!U1+ZlFRX&Vfo*AO=rd&UPsVixVGTm z>z1?TS#_ixq+4Q%{@E)!4x8|uPjDw}`Fc>Sc7g19#9v*pv;hqitiO@sYkRrCDoGgT zSP3;p4F2{b^<2C0HZaDrqk!+`j+anVNvGuvf;8nL(PAxRi?${w z+`~~3d2xtRY{k3#S)IlxT{f?glaK<%&TLyYaS8Wy6(m*b4bc{WH9 z3hED&d#ZvjG2_WziiFW{CPu_O+)_~jf!iq*4;#!^3uS;Q0WpLd1gc=el)&wUBBM3b zdrr3bOi5v9R`3Mm?AYafz2W2=w6pyrcmwdi(L0Z~XvaCb7JQnZ>q@PV5GR4(`&Ms{ z6)?E!^lnEe3TYU6eMLNdEyU@t7j%P$r3JrLg#deOkJ^G?@cq#gox#Rj!%YXZn zjN-xDNW4=XE9+g418+R}E0#?+q2(+SjI&`3st=}qpss)pbM{8~)kz0Vx0MF+N(1q? znzRylNyx`>p}PMLKy@Z@Pe`P`f3x(|8wo`)Luw5vU}94L&Hm@%_6KuL_O${&7oI*+iBm;XRPB|@LJ32c3~=&<+$!U`Z6+M)287rGcZ{SrSf zDh*N=G@CSk3`GS+YZ+mNQ)9C#%@Z*;BUS9=N5h;9O^c;ODE{Bm5Ao!OjyFTS(I}Xj zCK6>k?#)S3<`IdPeisGC{sa!pSu8-a(>)xh47q4okKoHYZ2!brd#E{4k%4W-*bGE-;v;=pey=CcHm-A_4nR|ynjG05)}#!e-C(f3HVLH zt)Kvf72{#J(OH5JO*X(8-3^rPJM7d9PWrhq$?44U)A8QD*Vbn+H4yoZ@uLRm%-=;l z&BM=c1kV!qDPWj=0XWJ~Fgzk+kcI)Igt5l1=7O6Jv|(@|LW`Y{f^a5dn9NX;N=TeL z2Hlg1nGvv(ATMpGI2#9=`wM1NuH)Jn(5$1AHg?;rBN_o)aR;XFVi1E?EBX7dLMG?yHbkg-c>1|Ds0XTGND z8?&`sYa2gwF~yk`Td>`8o7XM1rF{7^m>6cxSx4SdsIrp+ZS4OhuAbr?th){OWXGlt zEt(BqL=P{!C#D((J2>XWHk8R@DlG&4=XMyzCY+O|U>8xS#WKXPH03;`dtwsz8tI%b ziKEWLK&8?%CFpBJJ0Z|>%U2_ezW-OoKtSSH8<(l3&T|@BhN)qi`re5X6MpWFPr#^* zm0c1svpu1@RNO8s0%8Df#CnMgGIQ6h6TXkLdC82b#toX%>aR4 zE(10N1va6Q_)0OErCDZnH}s+7yN0HIaCX|fy#gH2)>!%$RE+#7d1IgRRx=Nwfr_Qx7BZ;e zYyE+Jo8Vm%ZykX-?P$P9HxRp_V@ithf8IB2i%i02vLdphl0dAy_I!(8FXVyb+H$an~d`zA_ zdVAkrNbuUj9sPJ4c37)<&0lF-Xjw5a66OBOOKT$b#n=Wsr8=CONF?kPUor;zrI8q@QqqB_EPG$L1!1I9vT&q4D&dRr=4> z#z}-rkX}nBR)z<8)kV`;EzmMECVQTi`-c`1wiF$#ZY&&I(nJ!15W%DTIWXsOpKG{r zch&y=ncic5KW^E=JoB!#SDGIN8Y+tr+jrN+VMjLRh1n2S2sV_;SFA8XbC3ciF&~y1 zzZ9VSoN^7?j%g0+Ksodl;1`NC1+VT-%cq(cl4;ES(yFhvVWeU{ZGEsU@ol5gc&KZ= zjgD)5b9FzP6yNUi-{!81NWsw`<9RQwn@4=4CZrvR$Ro244iPZRc+(A*_1Qn|A-}fG zM$dhQiHVslE7$k>hT{t6HSHIK_=3&W@)zFSCWPSEQjHqDvc&d>v-ie$Ng7~i#%+$S z4G+AP_~S%>1jr3MidVM$`Xg~u7ry&Y5qoC&xz}HJ$(Kb!xoAL5Y0#ayV3(pJ@Ps_| z&}fr`lgc&|L$r@W24CX4LLtvv&)t;ZW8m1%tuu$5pYW~$6kA{!Omc7VY#X#b7y%Y- zsM#RfM)S6C@6nXo4qRGi5c=HfhVjMf=s7-bB&t_<*v0fdQP$K=Pywc^{kwsJ@X=6j zbJ70@PZcxz+1U9=LA;jr@n{{@p_X@b*EH>pt)j7;72%E4&|6SVr)L~u|1rK1Yi-5T zD9i(ozr0q#zmg!5wk5P}(--jdn-&)T4I`Rj$Mn^5`LHOI5)}M_cu95w31@G^OkKz3)Bli{j?xTaWdOp^Dn0l-_MWRhAeuUZB<=c9_ zrs&AeZRM7KX2z~-)MjXfuCSFBre}jg5H(J=%l~^Y%uC0|oamzTOX)T18Y2g$6TdHN zuGU`cUTG%}-mlKN1X=T#KXm%{-f-N&-@MlhG-+dT?a906YV%GRW1QU{!vvA!L}dml zUZ%VpFo4KgAE}RBVH`#3cq&i0LP@TfPI@9~+~`y8lXFAj+g?8UsNXvkB&n zF{d7fYMf5@@V2jS5v>DjCxQ$oe+hIX3I>y);==Sp?+r&{R^1N&_fQqcK>^*YEj-4r zhh)mO|4@A7$SC^m+2fMIMK4D-e@+zDi~I^}CQeTJw~Iy($Q^4cJ?#-;J~j1An@^Pu z_KXi_0U*J!Am{V5S?gak%8vHTKp4IH_T$3r2uNxYyb9)8KdXFrFv#l==n(A6sh&L! zq7OfRtS$-+8P(1EoMoNbY<28Lw!7H~uDk&!qO>^5Ow&BIaJ^ru*?(if^I>%Y2yF3p zsfD_Mt~SfdYz|V3mY5mMYDwoXX8Ny_>1|m}o`zRj<%#N@B?AU(Z`ZJ#r)6+qQwu*a z1eS8{U-Cpr8x&aI4F_$ zs6pjATCfnp%E&#veztJx!f^2wf3&A6@KQSS7cZeUOMBSTm@n$dz({cZH~rzFg*=Az zay3?ASsT@+XX~rj=4ZpS;+S32-tP{;5+o|9T54i*PhM1oq5#ARKdblLk4 zgL+WVNd}JG9a5fL6VvUcOhy~+MR%|HkL7~*@wumJdY^%7l~vPLg~mRPw)s-dhcL)A z!THk4CT1qH2NdToI9Lj3Cy-lkEg_|>@q$Czpg~g`P=t3)&(H_jv6MK;2@@y!e9I2R z-kB~xoq|m>TG0OdJ(jl-5b@2rDq<(l)Z)IHZh^%E94?-JmOEKoEZ?-j%hBb<=UXa# zIhkbukD83s%Id{BrsA4u^t|Vfi>`M*U8})&_&J^ZXHiT5wYup2HTayI#}z>@W#(;N z?3z>5Tqx}vex@=E#Mpcf#{5iMMy#E5v{2emR2@}kb~E9>_p66!Olu>xFFn+UErO7Q z_9f$&>H-4++u7)}D)bYLWX+$)`#-zpFYJaM@e}S~kB8n$?DxHEhiX?5tFV~;&Sv+w zPN(kQZr~X#kvVQt*MB(M{)6T88OgVs%4&(U@YEbfe>x0 zy4vr|-wqiFZVV)3Yuml^ACXPGn7;M3VO1u~7r<=8tawNM3cb|ldVwIUb{y7`j%IB1 z3Vv|)bHMvd>2?n4-ZRgr@#iK$leDPDOh|ujj3aYBOXke9-ProPwJhgskL-!fKJPwt zN-D7|dw79LW~~z0o?!7%q@82oBV-Mww$iqsF^(>6TXCrk_D5O=tD5Dj7u!Ad{K%P9Z4r19a*o20jC^58WOZj&j?i?)X<^Kk><>9k3-efk%PM<#KT%a%!XWdql zE!#QBH(++pC`;$e8lQGMP~CXf_pIfiCTpIB?`|taF~|!1)LMHFk2Jr zc(g)#S@cBMzFror__nidLxa8?+to2+mhhs~sB!YTVbk<~b2@~ZIHEQD$DGhUCdFYH z?eE1J^hLB7!P6J@9E!<_o7#fWFzIhZ7j=Q_ODRs(gT z{q`~$sQbG={(Jq;2ab6wL0|WAS|Ze~sXjbls)H^f<)s}q1SGO0EMI_2)k4MP2FKj3B*g_1Af` zUk<)VY)dE~Yriz~Hgj{r8F@2Q7msNO7ZzVt$a>%0vjBFOBciDFYBWAM@J_-91h!LS@zuw7yjyGhs`! zJ<~riekiQA8|V|+S_TKQtv^#+jZb8K20+PTzC;m2I4m$Yc@t*~O!6vBd`sE^TY-~= zwW&)E*yWc*(fho+jTatu?iuSaZ0INy{4zAcXf{d`mC6d09ePR|78s78$+AQdPuXhF z-4rFT9t}u<)kkOpW-S*~Mb1u(go%HDU4MXxJwwgpa`x=$xBj|YLH{e!#$K7vSr@P? zZ{Ji)I0tTi>sjX`OwW13BsghaQOkic?N+qTh=nrwOud9cljxxsP zrUa>P#Fl(|0>qT@=ezHRyUK(FwFF12iwy}*L>LO`JZ?dV`?djn58`FYInUz%F&J4{ zHO+sN>T>3SnG{dwM++*RE|6|4a$x$Wd!%!4MNjX+sEBL9ZUKYc8Ud$2SIE097xtTy zXD%PW!!kuFw-YH2kH0MZ9yb$JSS94pSQR_n+swYVtnXXkKw)`Snt9i(y8l3yBDT@I z19R|5dnN(nohQn}a=;g18!i#TG0FYFfOV7cBhzJY<(=ad2Le9@ z8lu*fpj`91+}rZc?;G+Mubs7;n$m(2L2RVTj$Iehc6HQ<*KhR|bA1UfdsocKk|Ov! z4fsu%%msw@lUqi#7BM*iQu!Fo1)BH^Qek`1QCbCiQp4j0-Gz%Ev>h20QmbDbG6)T_ zI+uL6@I(D0AF;c}IX7tJoapv7rA8Mb8Uyrj(N*h<3BO z0>hbU0m`oeL53opt70%hTgWK3`8I~cD64i%Q%ww??J~)%0c>6FZ+W^RTnU8v2L-@z zYClsW@ukNy2|3&nTZ18K4;mWp=z?EwstIWgbSIEoS`FMx<}z=1lzP_u?@Tf`lje(z zLe*fuhA}m5;)Nf?8FnM}vxiluEdIFZF)mdQ+&x45%p%}VScpPrBkS$~yVhpFdW#KY zsaFE1k)u7Q&-{mNZ$zH;E?d4_aT*^k4XXs0z0Pe&?6c z%~18H5?c`vEt9eXh_hc#?xCQl%CoE~QmC{LF8|Bzbv4X?n(?!`Z)c@vbqJ5D{2VY9 zupP48Zx#Y(aKQYa79yS|#b4~c_g>WnYaot=vh)>~`6E0i>HyIc{@VKT?3dmkJ(jS} zTxwoK3pz9D)T;H3K+7bEg@JG@C$!;Jb4CcLJ@Ch{44i+kA4LLr!M|pdaXU}{7C~Hh zgXwX3{+#(6t7D1lQ1|Ak+>4Y4j)5EIK!_yAXKlGKV#LR9bSt@-m-D!PNa)8uu%P}$T8U|AkVjX)c@?hgsyDDG8<8~2z zNL#LCP6f8-p))=k?|75U*2o~s)zUhg$#?`hw{4q1TJ;KwhT7Ocs*S+UhKH%SuUuwm z9|yfAi#)gXeX~)7XRG0oQFoX>{{5c6r_azi zNCvDTD2oHnClb<}=DYW@BbGHBSXJD)-#+HB1wX&`LPXr7pK=gI}UCk-Q&GL*H zRdZc~=qozn7wbEp>KQp?v|$f*eDgl`R8&f2o_w=i3a(B2tA?CZ?6w0dH6kxjGWWKw zgr!unpHkUq=|{7X`JGQ$7rII-dA3lI;?DCqqVb0XON<7pN}){_lj%i4CR066$HsA4 zR*HT3H;+a7cJ;EqPcGeC=wh%ZDpaqR zGpA0CkXc{lSSs8SSq%;ed-f3B2bryLys(pN&J&*I>&I@|HJDUhPsb%tB(Ea|xD!5Y zC==cM(?m-^6j2+p!JCI7YB$s{<^B$2ECSeTD!m4mL`&f$|6Sl)61%9_4Ple zGp91omR0Mi7G6?mr%2&^Na0d%IA-4VCF9WrU7ZHoUj!jTFUK|D=ds+jz44z)ZEq3v zKoDDIs4?KgJNnWc)74(RLL76rSLl%Ke8!RX%%Jq(vXrDgw$d`rrO9AP4nKxqgZED9w0~ztw5qV+i;}7T|(G^p0c!X$}5d z4Z)N0@FO#OdyTx2LV?a8)nOnG=lE(-=b+^0-uILq?(CC}y_L#5+^>~qKUOrqAGi%J zFQK*Szg1KfxGNMSe^VlD)6Y{ z^`Hvm%Tgy$FcBAvh^UcsFhW#gC`Jln)AKj6X=GKu;bvJ!G$gHruFtp?QzYN`oq3^d zI@{(*_DvYa-*#qufgnXB3~f>;B0fZU^zJugo`JVPG4uY@9n={ztHc^|R79kXjgM#s z-6p3vDD&=QT_(zW8K)yD2d9i8Tq}}^c854}YwsbubvO^ayUE2Te!lT>7Ehhv1m*=Q z;cXBGABA2|bKlQHb~CG=9bymYx10WpANEh>CW5ID z;6Q@5@+Q?GD?VsumOfw!x6mmAJEAT9pNkziHY`*U|Mgwg)AAcE;(J(rDs0hM;stmv9zOifYQo{pVt9<2HWr8KOnDE}^%77`xke#7=E&C-NE zW^`o+yw@V||6d#ia!meQ3oIK{aEqo=HonVap0A?AKE$2os<5|6KG*MEBHl8Y9HZjq z&%}3yo>2GtBBj62Z10dGT$bR>3{ghp9P|i8_!#=@59T7R6Fj#mkA^ug9SOLcPU-WX zvAGgE;qZM@-|_OVuEq+j1_T_mYErzQS6aC%(t}`?i$@Q0eTZUi-MGpnid%aa$Y=JQ zVW=ADCCx!U^K*y|9SVE;&x~N~T< z`BScm{KKQQwg)>EEjl@jnWaKcurc7OxywM49antl7%UfHWGU;0V!J)Dk(%MBZ#42Z zY$K$tcwD7uubN{bLixKGJBc0RnNOcVP|y|o!thxZ4SJNu(XzBeuirSc#+EO0W=r#F z;5S>YedXU`D**xF4TahZ=<+k^b4zmH2r6QReGl$`DnaAdH{!b@B~^GjVIC|v+X<+G z_NwWL*$MY9X1J!2f3Yv#qu{CF!U#vp55F5E=6l4UyT0(B5Yl#+p{eA=Pl1dNY5`+@ z@MEyt@=%zM_bOce`{;3wN9HmV&OJ3AKEBh~FM1H^0SCD7wru8^PPO1GaX?&xIuu=_ zmICIi8EG^ds$guu$`ia4NMQTVyQYu5mmr?o`G%|mg&$m$Zb2WXH+V%abk}sCct=>5 z%r(QT?m-)=jI*{+T&eRlA$}#1SSzYP=QWW(6tK~c!tk;GoMXS3mJb5u5_mrZK7-?b z@Vt+%jqS0OEJ$Wxbdq8CEPmY>=F7~^rc*DfG@)>t0~eDRN52`;l8A5h0ZrDJ(XFT= z`7OtLHh-4JK(D-UIUt05@C^*DFco3`Lm_P^apTfzTIePVPp|RNHP{82kp`TDEuuBw z+=CAqB(j&bn%Bf%dq%dUbVWCQMQqkmTA%M`1;eB7wp4Fq#K9`kJhfo=2w9=!VOH+6 zsd~M2e{_b%c)k+m@bO(mF)5b!_@7s~xJ?)k6L7nky|Bd1!@j-@E3tlY%taadfo%Ox z?Kue-7-zx2THVNuR)$5>k%8vyXNU5a3fDV^_XQe1nZ!iYw(U0#tdXCs3St*@*C2hR zi$`u1tc^ic=@m-p&C^%&lIDf~2ISdxr_OIg($BgJF(kw9ggxCOfXAW3>M{bmJ9@ z?FigznZGm+n^kz9Jz(HkfeG6VhcZt~d@~_RMT*;j!t-%~H4T{Oc*_PwHmIIwC7mdJ zFk5xxDKsJ%;W?9aF+Kk8PYPmP9ce{Zd~GLwtig1w4H%mOf0p6hx|y3?C&o(m0#=UE z{^+46p-358j)A$~HiPNKC!bj8Pl_8*pFPV_@lQFN9H7E|1074@xkU0j=qH2(Jq+Gv z@Qe{QWzSZEpObgz)4xsKu|oQB?EsYvxu-6rm!U4jJ!u48uY>@r6ie6or~FFKTBrS= zsrhVB`TC#ztJQ)apkT1DOn7_d5yOJY1xa?WK9ayuBWd7AOc6QSi?N+X1U6 zeb)TTAiu<~Wrvf!TmFPbMwTr&QaX;A<#`j)Ry>y?-Fxr5;4LJFO@~|5oU)GU;5u9M zOT^>+LmxMBgXdzFcy=FG7qcqV?lMiXZc9;&&QT_A3U+7)aLsDwkEs8)*eF%atkrVi zbWOM6%GNtzHJJc`z(=H3@vAZqnSS4xYEB%?oKDd3RD_diwPFy;qFrJCb(}P#_zH$) zcFZoOC3gl_4<(KJS%pUctCd(~RgJUN)0*q$+%X}yegWC+uZAd?21y^17 zJ!{(X7Ox@0m)L35&OxS^Fv7m<2C7`eZ^kOYotrkk93bpJC;RY#0vr>THst${7F>I! zi}Vo4i~R(>V@wx6MkJhzt$y#BitpHm!j!3CO474t`^55-f=3(NQq*bBgWI0pv4vsu zG$8719qk-qAA<|CLvm{XMPyxkuP)$4Ahso?17|?`@LT4F2fN09N-YFsDDhALrHVY} zbRNv|q7fuwBQBGxtETluW8OxW2hS&!C6|3&JB-J3JS#z@1|ceDqv*#}DMfpw4LNdy zD&~cxO~i9|x=R0Hpn+p7m)^O{Ah991|K_Qtuh#qyh6d|CugqYHFD8os1qf#t7TL~m zxz|=>dStK`PQU+i_q`tTI?}|@HRHW{g_wM$JaaLw{l%3=&po>V(TI47g3l9psvJXJ ziRnWZCd8Vm*{df0Qsz_CpoKv5P!GceZJ2of>BLtN7ws6eM+@$#co#%9E~5G2#_$<^ zL@w7L+T}>1XNYfA8zeY8u*>};PlyN^q?3uMrlCXGRjH>PpXZ-c98+n=X4;E31y4bB zhH7-`Q~S&`8?eul9K4(uO-4r4=E^;_$)&R`H%D4NbPN#p&JdGBIRz$yp0s|cb$3=x ze9&xMyqQs&D8_F!YQ5Pg;*q)@fE4Ou{d=8~CE8X}4~0glx}GQ~KohMTn>=mMjkJ_% zodX7{dwy|$x7=*RTs_lzI_3N`zP64P$pu*+1GxN{bKn8jLa@5qFQ(e;5L#m=yJVNL zf3WpS+f`%BMmc#6mT6kyYQEBo={HMg)fXudok<&V{Wc_i*)DE{%1>MI;_@;nY#a8Q z0gm_fcpxwzZzpvkvCi&XGDa^^1C0d@_YP-m{gBrrC%+mpEK=A}?)`2h&wEp%kW(;H zspoP9&k6HwW4d*O-F?wDkMZ`58Fn8HWdf%P=@Mnh-V#ll+_0!kdk;kk?vEds%{sR$ zU!SvLTlkqhFcIY}hWksQ&Orl)z2@++m5n`9n|17@so}I-gmGLktP@d~W;`b5q8vR$ zP6WbGV&mU}d)@}y(&FaZ_}Jr$D#HXxV*AiclbpW^;oL=THOZWusyv@GQlBelcFsvv za$6ZqMh zG_^;toWDy#5_BRw%Rh?)$lJmw?Y8f+u4%bC|Nu%iVmAcMeQ{@|U|C zE>NK6@G#@0#j2se%$%@Sj(Y*g*PRXr#IKcFFp3@M_pm4!`aUkA;}r>M!$Er zv&c8gv`2L+yiPfN=x`$CKXsKfB~ zHvt9L_8T^50r9iDWJhi0gAdgD?#Vzeeb&$RW!e;}YV`u2ys3&K$Ukv*ooV=Spvkgz z<@h$c>j|(D(s%kT&SW2GIt|+C#b4jpcY+gyveh_o0jv&^2;TGVE+!0=pJYsfhKLVE zh8h}uJg;?AFb=c8ny=iKx3wQme5RoB%Z=w1)gwmJ{2jGq%)Examl%9I_`TvZ+6|`J<D1{dLj~~$|;jq>cMT5@Sw1j5f zVur9(-~EOv5i>>bq6>ZLN-2h2BpjW)G{q4k#r6IVr)#I!VJmpqpqN=<3i=4ip(V5S zos~m@dS^flI3--8cts;32j&_7$fp;@Y^k<$HOI>9_TpJ_^ZaF~-VTDSNu~*WH&36l zJ;T!+9T1do+b561juPyHOATV@KH|1SA_UOfMynS7E6;;a)41IKuU&WV-VPJ#P=ajk z#ouZG@T&td2y`LW-Uiz?9IkQ09AB zVjh{H;>IFVUD}7h9G!SjZWfr*@LXMn0%Twi+;IEv2<^yqA%@OxNMdvxiA; zR{Uah{e9v9#dIksq145EN86YocqHwAs*2~wWvrkQHO}xm7$S{4*K@Yq(x|b0uV@E& zPmj#|Y|ZK7$s-xiydqAzOHAX~Qy^D-sPUJ1*j>PrE0$-Vy9_lZwif?ujnXT}Z>Sg+ z>avj_5WAdwKi*^Pa*eX)qD5`njHOP#=+1U7lx+O&u^eCKZGk>?RVrUGir}>;SP(~9 z%p^Mp^++@S;iIFHr+xTV6V7A{CKPLjXBs@BkC>?hE8HEMwEOFLAunZ}(u4&DFIwW% zBBJ$>g_2oAzZWQ6rYw*+398jsQGpNDGA?@|?-i8A7z@<|9{Cq+=6-z z{GEW%XM$Z1Io;VU2#>=HE%h&{>i@y%TrOt7r>YL3eOCKcesLJfJ!iIx)KU;LO z$w9KN?3ymj?I4?kQbsS*g~`6c{wNP|9=D+&*`Rmle}H(udEmbmFCPEM3no?O4Gzr( zppBAEL5btt`5GayafwdgWZLz?4;(MdtFKm3cabhg2DB!Df?fQm2Fo_P3KqY+^Rxt( zMEif~4}xAP6vtdw@V$v}mTf%WJi}2fpLrlwn|vUklzwP*a(f~ zN3Z-@+`Nte0jJFHmLcrv!|Tq|BN^Sh$$4IF=?v?>7j9p3bD1AE@z+-f6HV}zK$J6q z?C_vh9L4?r;cCLh2u&#|bNV6NIZf7)PiG3L)qp%-*%F+7Qjv`20^|(Q;+o`*kCf*cW z15palHJyy(HVRk!->OtRink|#&9$F-4RIeqv-;1MWjePhJ06*Si6d#r)@s=X|V3y z__yH*5PdcNaU^vph>gO8-dMafi5Kn0FB#Qs_Zv44B)xHbgUG8T32k5EFvfT5^Z#8g zr}CjBdf8XuzjV|@Ja1(x*cgf=PkQABmk&cnp~Zq{OoPMoAa>5Dvxu|QifDL3#ciBl zvdOlwqmo4WSikA6om#H}LyU;d0z?b|5D3tjI$t)HV0xhdDoEC`PiNxOFz;4)Y|6=o zLLwdP?(V6#5qPWpAu$KhKbC?vIT66J{NDwK+CS&0)AC@LsXv_Q&Ck8U%0R$%1GTh4a9-&&Wer&^iP!oR}0b!kF%sMRqW>gbhy z<9|>2$_KsZKIjNSyv#SZ^rH$++wcRP_7a>lB%Oc1K zVn4by@DR&8lla$j1ZW_q;cE9F2-f0^6Qze1)54Ir^L>Pbxrl5Ch$S2qGz2VwQzgt|H{H%oeiui?ch_<5*R%?)qnpwX1ryJ%xo~ z##C4XHDhmHFUbA^R2~f2-@!nIz$LZgYy?VGBM13tjO`QCWhL5_FE>48QQ3SLzy{qg z+jKKyiFWE*bak|QrFmT}=k{H_GthJ9lthUZ5EQJaS!I$A3w3pt;W9*l_h3kbQbI8w zz5!&Y_d~h=5=2?CY<>n}HZ$ z<%5(Vc{yPX)8-gj@uh!fu0asS>o%WIh@GBLHYFUQpMls+UC>M{+p&*Sj%Fa45{TW8 zGQucvH0pKO^dk2`LeVT_6JwW5+35C(_{V|xkvX&HLFwgzUc!Je2PH<}1{1j;Y9X=6 zz{`Xcr2xYUT@I2`f&8nZjf3g#g5Ov*NuYaR?6#+H$EK=i+_SdW3KkweZ1Z+S=ye5JX9lnQZr%5~zMTa(vvUBPTM^gr=y1#M0auJA=7{8w@Z z8}hmewOZOtGTOD;!00kv;l_TO^I&X|IBcE3x79$zZdOh=UQgdepruSwLKd#Ym4XMl z_AQ_ZY4D06TXR(k!ih=z$%(c?bUZ)7Dw)zvl4AaW2Us153h60SJa?FdIF4q72<``A z)K7#Ta44d5BR*sJ1^EBEUeu1%UpRZIjMjdRbdAYo+?TjMPuD1IWL(_H!= zuE+ZgSC3hurYuxbozV+{Fc9xuvOMp$TzP$%(o!tAmW*BP2yFp$5em@yW54MeY~<28 z!5`9KgPtXv7btZA8UUwx*reXNzUV%BD$!PY+LCzP;U^prvmm!4srOHue|=A=gKONm z>hEA0eEN!BgVnw1#wm>$H@JvImlK0s1Se$l<#gmQYT)`tFj|~UOwc~uxyA=&)Dput zyj@lPB^hsIKW4{48wDZRL4(jC+s+y1Xb`g;Gn8W*$_$IHrz1dR&|%WVS%M8GnS6QTE+CF#wJSPO<^L#wj+a@h z_eQWRJ%JD6#QuS(PJ>XeV=)ptq{A09sm!Zf)HwGsi9ABM7)B|gow!d(Q@$tuXa>6P zxkIsi`PfRs_kqU&)5RD8X1a4ha>=?qr1pgWz6B-iz1o&#{N%)4!Ymf(4}1CNV+z8U!(8!%zs4%hl68d#1UwKk4#O z$PLylq48qpb3YJ1V<*~StD(SEf*YAKxj}5Zf10fX9Nk3Um*9~nvnk5Mv zhOkp$`GC9??{NJ0rtWMdhbi+f8?duLK&Vvp;YbCm@mJ=LpgQ!uyL+2*SHEmm;JmIe ze5{W834%YNtjp|{=dGmuCPi_a?b8EX`fA2L6l2Om@Q8Ej;JqX(`{gHj_9&=f&1Owu zX7WM#xnE+c+xndt#OZW|c&fYJ}cn zI~mLthFcdk-%N6Q>VFJ8#&3w8q=3$ILPy{RC!{5KoU}IQ1;54uX?p=Ot1n1w=8jyY zbXSKdg||T4ecopo#$-dbIojrF;tZRLv&vW}6ZkKC7+a_*qoyEsx&+}8EupnS4Pq^8 z5^-f5^eJNHi1RpT^hgcP)q6q-O?pA*{SZf;U@{iJ{&oF^OBB$f4hASXj)j5z8(CtAWl9qhthm6Nim{-4z2XwiKV_q6IMeVEB23ua7Pv?ao!?Nf*7I{T<}D^ z_Y|O_Er>i@W=^1@NVs(eFZ(e=;B4c06ox4zRbLhz4As(`-IAsq!i{GxrW4CM^AvJygu7rT)A` ziF}qBh{lXbdcGJ$1`x*jA@MOl?-6sApaf;fxdD?IhZ#q%sY{EgIJ1Cxc_o&`L5J*A z?tWupAnf#LjLaPsus~Iu?X540iKw5_9KC%TN#*XBu+=;Z8|``8>+c3!?6jve??+C? z`vJ|aF(fdzljt0*kh93V&j&OP_l%Kq!*&D4q!J9*Wq?d|J4t*9hj# zJY&#d=V8%(4T4;Nu;DM(EfRq^@y7$uuF?@OF|&X4GZ%CFhgIOaMyGd%32T*KWH&|Z zkrw(kYP@fd87^R=$kY^9UJ$8-Qr{iD%lM|ay=m#z>)xz+>JPEIj~$1bRb4P zW%Eq(w~CZ_m1}XyQ;rfeJPIY%ZzPhLl7yB2L<$LOK8DseU6E}wh_*Vs(nOffR=J6Msko`y#f;uszm>hpi^DVh^|IB|gyC>o%!@VucmVFXF+- zYXr=@Mm5_L_;`M1YDKxeHGVwhQ!t)Gq(3PQEsP)Ei@WXEW40*Ucq0UU6!Ahy*oVZ} z0VNRV4AfRfW3Fyt|Re8wI2~Gz-Wo ztOz{vrR!L7R(Y7=1>P^2;eGULuf8Vke@V5G7T&=G1(9`>#w%QhGSj`iem@Y3-`=K$ zL)Vj8BPb_UBG?cxYDcgFl-P*vYDbT6Xz>GWK5;PzosHGi1YT`I>)U{XDDog>kT3jq$==-e zSq;AaIxJ7+byLer@4+USl5~(Knq!_{&~SEc;$_Mv;})FQYZ7UQ7oc@>jmlk3f>h!{ zv|rEn&@&1Ua^1~CF(5$=cOv5kd%lz;4srY9&r*(J^iv4hdZV`T0;8FkT>tTLU zL!L{c1evJKs*^iUZ`Tog8iynt1H0J4mp!mg3gOZh7&x`49{zMDufYl&a@&!y_xJ11 z+fEzA_qvNGAySF&w3Zi zulJ>$`bJNgKZ0mBqUah&HghnR>uI@EM2UsEl91iWV z<7vq{*quAF1L1b=Kt5e5e)&+XjBnH!5;Z#i8-eUQYByRxGUA$SpjR~Uumvu0h zL7YSq;vNsWSIQ#el!QkV6z{-(QMfR0+8)OOk1j|<(~)=uU+^oL3X@Fv zH`ei(E95{H&FbT#F)vuBX zJQ3fu)YXRLdZ*cH0&l{44f_;J;XF1ONG`*EKXEmIS=Vl%3XwAQzZG~X<#QOy)-`lf zyle5-FsaoFag6JLrh}10k{mP2Pm6wKMN0(B;jgCGHI=SMMT8LX&;rQH`@<23xU@-c zNIC4j5*cdQ6XdiK-5wzAj0J{tJqw2v?6>cNL|Bpt-#`)^py<``SarAJwarO*RCv^* zpV~K`F^r@ZT1;m}OdJd*pw7N0aG}4uj<4D(;~-b^nerDjVW$r&HXdh_P@m>gX@^kx z%OhBiJlM2OSfy)`G0EwL<|;pq#f>xxI$%K|IfTHxMz0qNm;89PF+a1$Fm#wB2+`)M zl18RUpJ;(yVb0|+Hxs+BxuSxDT)G%FN#}e z6URDPmOo!62=pKQ=%7cZG3_B{qbuo~-GqF^#qG+$gvKOhdKNEYY^?0kl{!R+L%XdVYOMCJN zkGBtVu++HlHYD6>5=fCWnGL{hcJ|(WgbW~{I+L&V)**|m@qF3mx4d$>UYU1aSuTV; z7YOg2hK}8g{tZWcks78lUUbBI)Fmy$4TU{Pge2F;l zqh(>*m+?m_XabzI80pH1{*&QxIrDLcPeF_eSv7VCN+7x9_202MOTH+QR5-Is*A;9* zxA{_;c?i4GKW)q(f2D{Av0A6{g|a6i{K*Huk`H3-89q<)zfTbs2+UYeh=3Tz*RCuI zBl&HA;0HSuE*xJ&Nm5xx8@%9i2=MXIV-oO#2@Ohx=aBYL>$T234~2Gp2JDgF8Fm?m zVMs$3V}XX1)3Fs@6-PKiF7crRPQ_41r(8(-t)~xmwe+%`}>CrW1gYoZQL=S`tC6IgIQIxA>f(@+=pivB$2OPIk6efkab5> zLRYqyj-!JNZ;5Y2%*5L`*hzLbopMEWcO7Am{u-kLCT{``qMO(BIChHG>C)jV)ZOnK zACZISGWar#SmDVFU}Rp|3YjuPoo=+M&+8|~$rRnTW(byR;)9S?)S~;Ehn^;lFU|NX zQ|&ZGBO)+lIV2yOsAg65ZydI>gUpS5))?ruAY(MEDCYU}sp?-r_^!B)jYOYqiKc_V zPq0RWZT`f(#M#wQ{z)ALGE1F)3@XDr4DLy(%2IL&RhCc=8I!@XX$Ncdyw&`8^Q^=% zbk32b2lRi(7e<15?vK*esG1x64=DQJmlGevO<1C^7eGByv`XM0@c7OEpYt>i9trzBDmp)L=toU}DH z4E!nLI<6EdlO@E0bH5;ktfDvJRhC*IM}!4LU{tA>^mjo+=3005*0e(G@@6IV{v{}3 zE(B1`u*4|CwR0IQcPYEwBIdSMHKtN}E|=4hrJC{rH0*(P@Ci+z1lv)T@!ZrfQR{cm z^lte`|1X>QM5!WTO;%87Bbjnl#qQ9OQb@V$+@b(EFc_2%V1khLCH3aLkGA!;ujhH(*uCKu_DJcKZ z3e{=+IdxK?5Keh9aHz^$gg;w__i+fjahUf@PhOUG5ui%yKn?bmTG!OE<)>aVguCF2 z>em9^XZoAK<6imb0`9femQ#NpEQ!;8F4AdDpBPP^wwuAkXF` z_yiAz9%MpoWJ=2YPd7>0%~CwbjNdra;C&RhiPgGtytOcw*ac(wanEpo*Zw@7CJ)_U z)d`;ORgktj+)xV>&0uv~UKb-lsFM%%uOgyV^5PFohaxDNek3K}SDZT@?wV{Jk{Gn7 zmvv=QA0J&RcAeECFnU zmSAlDvZ4A<9~6E8M)L(iMYtZRA;{JZ@g)FHrsbrkIq20y^L6-bCd8dR>z})~v0Jnr zk!WidYzD)co)gud;~X^4EAC6L*`j3fxd!T({*%6pJGSl)>DLXDHlV%~X|W8=;5j!I zOFk$%mfV$nhxjVjE)nOrs{F5RY;ld|IkIV6z;fUE8W0%jkiD&UDuWLhJVTckWt#ID zI&aYDfGr$7ARTm7A}*rdh{(tP7DN~Xw2ROAb?tEs-C|=_3S0(qSAo5~lzN>3K*M zJn7T_3&Hl^v_dHaqRi$7OP#d;nDh9|QA8I$k|D7=8=5^z3!Lo|r)#Emg0PpJ$rUR( z*;r5LuT$o?KFGh|zRmSyiUX)29&@p7sA4@N>Z6ppqo42qD#@Bi4^a97!IuU#?;V$D z_ikeOiPgWHzBP2f4n6|fF2V&aRhnlS6p&Sn3W|(UgnD;qt}h(TxbnEOn;{DDqcZPK z_DDL%kIR{JWS~g&pt0Wy-sI|DZTV^!H(u-H{JCe4U6;M^%hy%>k^l*%L~qRGmINwG z%JVQ`kE~QEKB75Zgcg`oSxb>g&wnIlzSChsgp_ZR_|824jG(2yKqV1QGGtsFJrS(U z`sn?3>MGQaU{Xt*aiD72319t7Drv$dHz9$LK%&V>j=NUrdyOk*_1>s$GvL>6j{aB; zWFY-4aR#PqcbWcZID!!jh~$e${kR*TB}f03)Fgqvutv1n>N2Gi1q@J0X6T^Gyt<3s z+iGm<8VQ5Na=iD3OLbC2Fku`8+Pi_g3lBECFrQ^%80)TPn1H`^{a%yVHUrIUN8b}= z$2wsdCNo^AxoKO=VX=271WHz7k3it=8$iP_Nb-xn9uZ191u_EC_ifOKP%&pNE3F;B z7zD~d&46;b_WdRYaywy%XZQiKKTqfPbET(gi)f1aL&~7lkRsH#SUyBJ6#!+99ah84 z2qbv?%(sM}@pC=UP~7+3KTINHR9O;3?@?rpTum7X42gqZ(o_hSxN*W>&i55jfECWq z-N=@pTue`g8>;xgK-#{c8ofk_eVGS5^nU! zi@;N~dyS_1Gd`5eA|RQ0rBd(8EcJQ=`bpxr=Vu4dC~92nvg+F_6E7EHA2l8x64K~d zdBDMySKfei-YlV;s~nSTT9{X$0Qt2YC@AAo)}tKMTz@zBKJ<7bq&9BN;2*Fo)GPOg zVelh-4O-SO+}pNiBi2nJL4pi*5G3561>5D81P&WRAKtIEru(b+4%nX<_cz3ewxcF% z!#vIJIJ}Ea9fR?%CN^k()qTJgPf}qMrw1{0<<$DuE!QRQ5HGcR{sd07#h)pNSE}FZ$4A$VDx1ohvkwE zXM9HaUuKJSg(6m?=*hSAM68yW0DSNduzx)2tvK-;4GfDK(Gs99_!IPMnh3K-Iu|ae z?eoN-pYZ$v2CF696YK=!I~UXHzz?Acw|vLnIgKIw^jf~T#nF_}rVRJR$llxthn&Gk zJ#?m4{}?_Zc9YN_v#y48kCOvjCt6!3p*irb_(WFTjxjJ!vM>~p@sa6|h3~`OjR{C~{i!0G?u%KOmcz(wKu{ir86Rx`00&<4we_~>= z?%?(D#fxa=;r?(ORDucDpLGC}Nr3KuaHYgf{3^;!6;&~jaQT2>wV@9=ody5#gU&s> zA3rW}g6DmSf)dr~bjNSv9L&ln%sFS}oHytu`K(gj+%*@;4$HXpe;p zzt`Yb@2N5z^XUTKSk~ccci~QJKZe+lISehK$!z8^u+IN9zH@pW6ii-@oby}@&%xS0 zZRcaj;@ms4s`rjAGQ6YA!PsyJ9LEF*!(B=nb$Z*#RG`2>UE4c0AkWt)~_wm}44!kR56cW5 zwvX8l#l9U68+KKZSoEc5*Rqh7 zYR3)sT0n~&Aj_mY_zZ#v7NcfFGn~pxS-y$ic7o?0#CGo2Gy!~A$xVL)q&RL%d*awS z5S;KuiHTj_is|MYj|PB`&oInd4Mc|w9`W7$J>u^s{01v6Kg;-b*itv?3uY^)=|k35195{@DTCSoIgsAxUXR&)xQ!uG z?eFn$a0prJreJi$sGPJx@bYeC5Mlb=x~d-^uOyOo&O$puh-1Qg)mP#EbMYI-drntB zn+xG$HM~{6OL*j#i!f+BQbLLxXoWYK+x_$Jlnt#9tLZY7K}1qXDkRE+k-$ubjLg`sM#H5N$6~ zY~!~eYC#@c=do5$me~j`GKV|Lax@$vS+uRk(t#dFfrB9amy+YF4jI_t zzu*s6T$SO8|K;%d1>mgxzkhYbY&E=77oI*?!2eKtap`yXXQ%nT?WtS;IRF0vx&ZgE literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 09ba117..6a1e167 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 +
Date: Tue, 31 Aug 2021 00:33:24 +0100 Subject: [PATCH 11/16] fix type in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a1e167..43ae834 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 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! From 98750def008166273edcbfb03d8cf6b63db7bf88 Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 5 Sep 2021 18:30:00 +0100 Subject: [PATCH 12/16] Less aggressive range query resize code (less overshoot on resize operations), Added some validation code for debugging while running, --- Assets/Helpers/ValidationHelpers.cs | 134 ++++++++++++++++++ Assets/Helpers/ValidationHelpers.cs.meta | 11 ++ .../Jobs/Internal/QuadTreeCircleRangeQuery.cs | 19 ++- .../Jobs/Internal/QuadTreeRectRangeQuery.cs | 19 ++- Assets/NativeQuadTree.cs | 14 +- 5 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 Assets/Helpers/ValidationHelpers.cs create mode 100644 Assets/Helpers/ValidationHelpers.cs.meta diff --git a/Assets/Helpers/ValidationHelpers.cs b/Assets/Helpers/ValidationHelpers.cs new file mode 100644 index 0000000..b6c1409 --- /dev/null +++ b/Assets/Helpers/ValidationHelpers.cs @@ -0,0 +1,134 @@ +using System.Diagnostics; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine.Assertions; + +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 + UnsafeList* nodes = tree.Value.nodes; + int nodeCount = 0; + for (int i = 0; i < nodes->Length; i++) + { + QuadNode node = UnsafeUtility.ReadArrayElement(nodes->Ptr, i); + if(node.isLeaf) + { + nodeCount += node.count; + } + } + Assert.AreEqual(entries.Length, nodeCount, "Tree length mismatch (Nodes)!"); + } + } + + /// + /// 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); + } + } + } +} \ 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/Internal/QuadTreeCircleRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs index 2f3f2a9..dfc017c 100644 --- a/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeCircleRangeQuery.cs @@ -31,11 +31,6 @@ public void Query(NativeQuadTree tree, Circle2D bounds, NativeList 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++) { @@ -68,6 +63,13 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p 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>()); @@ -75,6 +77,13 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p } 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); diff --git a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs index beb375e..5a3599d 100644 --- a/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs +++ b/Assets/Jobs/Internal/QuadTreeRectRangeQuery.cs @@ -31,11 +31,6 @@ public void Query(NativeQuadTree tree, AABB2D bounds, NativeList 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++) { @@ -68,6 +63,13 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p 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>()); @@ -75,6 +77,13 @@ public void RecursiveRangeQuery(AABB2D parentBounds, bool parentContained, int p } 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); diff --git a/Assets/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 49d7a92..5615578 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -124,16 +124,15 @@ internal void InitialiseBulkInsert(NativeArray> incomingElements) #if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif -#if UNITY_ASSERTIONS - //int totalSize = LookupTables.DepthSizeLookup[maxDepth+1]; - //Assert.IsTrue(totalSize >= incomingElements.Length, $"Quad tree size is limited to {totalSize}, attempting to store {incomingElements.Length} items"); -#endif // Resize if needed if(elements->Capacity < elementsCount + incomingElements.Length) { 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; } [BurstCompatible] @@ -250,6 +249,7 @@ internal void RecursivePrepareLeaves(int prevOffset, int depth) QuadNode node = new QuadNode {firstChildIndex = elementsCount, count = 0, isLeaf = true }; UnsafeUtility.WriteArrayElement(nodes->Ptr, at, node); elementsCount += elementCount; + nodes->Length = math.max(nodes->Length, at + 1); } } } @@ -275,9 +275,15 @@ public void Clear() #if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif + // todo check if MemClear is actually needed as used elements will just be overriden anyway + UnsafeUtility.MemClear(lookup->Ptr, lookup->Capacity * UnsafeUtility.SizeOf()); + lookup->Clear(); UnsafeUtility.MemClear(nodes->Ptr, nodes->Capacity * UnsafeUtility.SizeOf()); + nodes->Clear(); UnsafeUtility.MemClear(elements->Ptr, elements->Capacity * UnsafeUtility.SizeOf>()); + elements->Clear(); + elementsCount = 0; } From 55af7bc67880383ed44a1c70ba1e235e6b29199f Mon Sep 17 00:00:00 2001 From: Crener Date: Wed, 8 Sep 2021 23:52:43 +0100 Subject: [PATCH 13/16] Added some validation and readbility changes and created test case which fails (not found fix yet!) --- Assets/Editor/QuadTreeTests.cs | 28 +++++++++++++++++++++++++ Assets/Example/NativeQuadTreeCreator.cs | 15 +++++++------ Assets/Example/NativeQueryRect.cs | 2 +- Assets/QuadNode.cs | 3 +++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Assets/Editor/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 557cc0a..49d8226 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using NUnit.Framework; using NativeQuadTree; +using NativeQuadTree.Helpers; using NativeQuadTree.Jobs; using Unity.Burst; using Unity.Collections; @@ -133,4 +134,31 @@ public void InsertTriggerDivideNonBurstBulk() positions.Dispose(); elements.Dispose(); } + + [Test] + public void SimpleNativeQuery([NUnit.Framework.Range(19, 21)] 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: 4, maxLeafElements: 20); + quadTree.ClearAndBulkInsert(elements); + + NativeReference> treeRef = new NativeReference>(quadTree, Allocator.TempJob); + ValidationHelpers.ValidateNativeTreeContent(treeRef, elements); + ValidationHelpers.BruteForceLocationHitCheck(treeRef, elements); + + treeRef.Dispose(); + quadTree.Dispose(); + elements.Dispose(); + } } diff --git a/Assets/Example/NativeQuadTreeCreator.cs b/Assets/Example/NativeQuadTreeCreator.cs index eef8475..866ec77 100644 --- a/Assets/Example/NativeQuadTreeCreator.cs +++ b/Assets/Example/NativeQuadTreeCreator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using NativeQuadTree; +using NativeQuadTree.Helpers; using NativeQuadTree.Jobs; using Unity.Collections; using Unity.Jobs; @@ -34,17 +35,16 @@ void Start() do { - float x = Random.Range(rect.Center.x - rect.Extents.x, rect.Center.x + rect.Extents.x); - float y = Random.Range(rect.Center.y - rect.Extents.y, rect.Center.y + rect.Extents.y); - float2 testPos = new float2(x, y); + 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)) { - QuadElement element = new QuadElement() + Positions.Add(new QuadElement() { Pos = testPos - }; - Positions.Add(element); + }); } } while (Positions.Count < PositionCount); @@ -58,6 +58,9 @@ void Start() 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(); } diff --git a/Assets/Example/NativeQueryRect.cs b/Assets/Example/NativeQueryRect.cs index 77a6f02..95f9e15 100644 --- a/Assets/Example/NativeQueryRect.cs +++ b/Assets/Example/NativeQueryRect.cs @@ -69,7 +69,7 @@ private void OnDrawGizmos() Gizmos.DrawLine( new Vector3(xMin, yMin), - new Vector3(xMin, yMin)); + new Vector3(xMax, yMin)); Gizmos.DrawLine( new Vector3(xMin, yMax), new Vector3(xMax, yMax)); diff --git a/Assets/QuadNode.cs b/Assets/QuadNode.cs index ccb4a99..94fc281 100644 --- a/Assets/QuadNode.cs +++ b/Assets/QuadNode.cs @@ -1,5 +1,8 @@ +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 From 8c4c03a003ef19a2760e2de7c1e0d1845930fc8c Mon Sep 17 00:00:00 2001 From: Crener Date: Wed, 8 Sep 2021 23:53:12 +0100 Subject: [PATCH 14/16] Minor demo scene adjustment --- Assets/Example/Test Scene.unity | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Assets/Example/Test Scene.unity b/Assets/Example/Test Scene.unity index 96ccfff..2af8aaf 100644 --- a/Assets/Example/Test Scene.unity +++ b/Assets/Example/Test Scene.unity @@ -329,8 +329,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 8f3a2756cc3eca64d9742d7ae481dca7, type: 3} m_Name: m_EditorClassIdentifier: - Depth: 3 - LeafUnits: 200 + Depth: 5 + LeafUnits: 1000 PositionCount: 2000 --- !u!224 &1933793338 RectTransform: From b5fc1f31175eca8e1d1851e5b77d34e5dad5fb56 Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 12 Sep 2021 00:51:57 +0100 Subject: [PATCH 15/16] Improvements to capacity assert, More better unit tests --- Assets/AABB2D.cs | 12 ++-- Assets/Editor/QuadTreeTests.cs | 97 +++++++++++++++++++++++++++-- Assets/Helpers/ValidationHelpers.cs | 79 ++++++++++++++++++++--- Assets/NativeQuadTree.cs | 18 +++--- 4 files changed, 176 insertions(+), 30 deletions(-) diff --git a/Assets/AABB2D.cs b/Assets/AABB2D.cs index 8f3c682..2551e9a 100644 --- a/Assets/AABB2D.cs +++ b/Assets/AABB2D.cs @@ -31,16 +31,16 @@ public AABB2D(RectTransform rect) public bool Contains(float2 point) { - if(point[0] < Center[0] - Extents[0]) + if(point.x < Center.x - Extents.x) return false; - if(point[0] > Center[0] + Extents[0]) + if(point.x > Center.x + Extents.x) return false; - if(point[1] < Center[1] - Extents[1]) + if(point.y < Center.y - Extents.y) return false; - if(point[1] > Center[1] + Extents[1]) + if(point.y > Center.y + Extents.y) return false; return true; @@ -96,8 +96,8 @@ public bool Intersects(AABB2D b) // //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/Editor/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 49d8226..2a2d796 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -46,7 +46,7 @@ public void InsertTriggerDivideBulk() } NativeReference> data = new NativeReference>(Allocator.TempJob, NativeArrayOptions.UninitializedMemory); - data.Value = new NativeQuadTree(Bounds); + data.Value = new NativeQuadTree(Bounds, maxLeafElements: 1000); var job = new AddBulkJob { Elements = elements, @@ -81,7 +81,7 @@ public void RangeQueryAfterBulk() }; } - var quadTree = new NativeQuadTree(Bounds); + var quadTree = new NativeQuadTree(Bounds, maxLeafElements: 1000); quadTree.ClearAndBulkInsert(elements); var queryJob = new RangeQueryJob @@ -108,7 +108,7 @@ public void InsertTriggerDivideNonBurstBulk() var values = GetValues(); var positions = new NativeArray(values, Allocator.Persistent); - var quadTree = new NativeQuadTree(Bounds); + var quadTree = new NativeQuadTree(Bounds, maxLeafElements: 1000); NativeArray> elements = new NativeArray>(positions.Length, Allocator.Persistent); @@ -136,7 +136,7 @@ public void InsertTriggerDivideNonBurstBulk() } [Test] - public void SimpleNativeQuery([NUnit.Framework.Range(19, 21)] int count) + public void SimpleNativeQuery([NUnit.Framework.Range(0, 20)] int count) { NativeArray> elements = new NativeArray>(count, Allocator.TempJob); for (int i = 0; i < count; i++) @@ -150,10 +150,11 @@ public void SimpleNativeQuery([NUnit.Framework.Range(19, 21)] int count) const int size = 30; AABB2D bounds = new AABB2D(new float2(size, 4f), new float2(size, 4f)); - NativeQuadTree quadTree = new NativeQuadTree(bounds, Allocator.TempJob, maxDepth: 4, maxLeafElements: 20); + 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); @@ -161,4 +162,90 @@ public void SimpleNativeQuery([NUnit.Framework.Range(19, 21)] int count) 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(); + } } diff --git a/Assets/Helpers/ValidationHelpers.cs b/Assets/Helpers/ValidationHelpers.cs index b6c1409..b2c18ed 100644 --- a/Assets/Helpers/ValidationHelpers.cs +++ b/Assets/Helpers/ValidationHelpers.cs @@ -1,8 +1,11 @@ +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 { @@ -23,19 +26,39 @@ public static void ValidateNativeTreeContent(NativeReferenceLength; 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 - UnsafeList* nodes = tree.Value.nodes; - int nodeCount = 0; + 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); - if(node.isLeaf) - { - nodeCount += node.count; - } - } - Assert.AreEqual(entries.Length, nodeCount, "Tree length mismatch (Nodes)!"); + rawNodes.Add(node); + } + + return rawNodes; } } @@ -130,5 +153,43 @@ public static void BruteForceLocationHitCheckRect(NativeReference(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/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 5615578..94d7fae 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -86,12 +86,14 @@ public NativeQuadTree(AABB2D bounds, Allocator allocator = Allocator.Temp, int m 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>(), @@ -145,15 +147,15 @@ internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray(nodes->Ptr, atIndex); if(node.isLeaf) - { - #if UNITY_ASSERTIONS - AssertIsTrue(atIndex, node.count); - #endif - + { // 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 @@ -163,7 +165,7 @@ internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray maxLeafElements) { @@ -249,7 +251,6 @@ internal void RecursivePrepareLeaves(int prevOffset, int depth) QuadNode node = new QuadNode {firstChildIndex = elementsCount, count = 0, isLeaf = true }; UnsafeUtility.WriteArrayElement(nodes->Ptr, at, node); elementsCount += elementCount; - nodes->Length = math.max(nodes->Length, at + 1); } } } @@ -275,12 +276,9 @@ public void Clear() #if ENABLE_UNITY_COLLECTIONS_CHECKS && !NATIVE_QUAD_TREE_ECS_USAGE AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(safetyHandle); #endif - // todo check if MemClear is actually needed as used elements will just be overriden anyway UnsafeUtility.MemClear(lookup->Ptr, lookup->Capacity * UnsafeUtility.SizeOf()); - lookup->Clear(); UnsafeUtility.MemClear(nodes->Ptr, nodes->Capacity * UnsafeUtility.SizeOf()); - nodes->Clear(); UnsafeUtility.MemClear(elements->Ptr, elements->Capacity * UnsafeUtility.SizeOf>()); elements->Clear(); From 9e3b86ad26671889cab0a02752b16479b2a9573c Mon Sep 17 00:00:00 2001 From: Crener Date: Sun, 12 Sep 2021 14:15:06 +0100 Subject: [PATCH 16/16] Added more tests, added new check into adding logic for elements that failed to be added to the tree --- Assets/Editor/QuadTreeTests.cs | 69 ++++++++++++++++++++++++++++++++++ Assets/NativeQuadTree.cs | 23 +++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/Assets/Editor/QuadTreeTests.cs b/Assets/Editor/QuadTreeTests.cs index 2a2d796..65270d5 100644 --- a/Assets/Editor/QuadTreeTests.cs +++ b/Assets/Editor/QuadTreeTests.cs @@ -248,4 +248,73 @@ public void MultiDepthTree3() 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/NativeQuadTree.cs b/Assets/NativeQuadTree.cs index 94d7fae..2de1503 100644 --- a/Assets/NativeQuadTree.cs +++ b/Assets/NativeQuadTree.cs @@ -161,11 +161,21 @@ internal void AddElementsToLeafNodes(NativeArray mortonCodes, NativeArray nodes->Length, $"Failed to add element[{elementIndex}] to quad tree data!"); + } + + [BurstDiscard, StringFormatMethod("message")] + private void AssertCorrectAdd(int index, int nodeCount) { if(nodeCount > maxLeafElements) { @@ -235,6 +245,17 @@ internal int IncrementIndex(int depth, NativeArray mortonCodes, int i, int 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++) { int at = prevOffset + l * LookupTables.DepthSizeLookup[m_maxDepth - depth+1];