From b5238a07fe2726108ef2062049ba87933dfd6a38 Mon Sep 17 00:00:00 2001 From: andywiecko Date: Sun, 10 Nov 2024 15:05:02 +0100 Subject: [PATCH] feat: dynamic remove bulk point Add extension for dynamic triangulation to support bulk point removal. --- Runtime/Triangulator.cs | 288 +++++++++++++++++++++++++ Tests/TestUtils.cs | 1 + Tests/UnsafeTriangulatorEditorTests.cs | 184 ++++++++++++++++ 3 files changed, 473 insertions(+) diff --git a/Runtime/Triangulator.cs b/Runtime/Triangulator.cs index ae2d171..812d9d7 100644 --- a/Runtime/Triangulator.cs +++ b/Runtime/Triangulator.cs @@ -868,6 +868,19 @@ public static class Extensions /// /// The allocator to use. If called from a job, consider using . public static void DynamicSplitHalfedge(this UnsafeTriangulator @this, OutputData output, int he, double alpha, Allocator allocator) => new UnsafeTriangulator().DynamicSplitHalfedge(output, he, alpha, allocator); + /// + /// Removes the specified point from the data + /// and re-triangulates the affected region to maintain a valid triangulation. + /// This method supports only the removal of bulk points, i.e., points that are not located on the triangulation boundary. + /// + /// + /// Note: + /// This method requires that contains valid triangulation data. + /// The native containers must be allocated by the user. Some buffers are optional; refer to the documentation for more details. + /// + /// The index of the bulk point to remove. + /// The allocator to use. If called from a job, consider using . + public static void DynamicRemoveBulkPoint(this UnsafeTriangulator @this, OutputData output, int pId, Allocator allocator) => new UnsafeTriangulator().DynamicRemoveBulkPoint(output, pId, allocator); /// /// Performs triangulation on the given , producing the result in based on the settings specified in . @@ -946,6 +959,19 @@ public static void DynamicInsertPoint(this UnsafeTriangulator @this, Out /// /// The allocator to use. If called from a job, consider using . public static void DynamicSplitHalfedge(this UnsafeTriangulator @this, OutputData output, int he, float alpha, Allocator allocator) => new UnsafeTriangulator().DynamicSplitHalfedge(output, he, alpha, allocator); + /// + /// Removes the specified point from the data + /// and re-triangulates the affected region to maintain a valid triangulation. + /// This method supports only the removal of bulk points, i.e., points that are not located on the triangulation boundary. + /// + /// + /// Note: + /// This method requires that contains valid triangulation data. + /// The native containers must be allocated by the user. Some buffers are optional; refer to the documentation for more details. + /// + /// The index of the bulk point to remove. + /// The allocator to use. If called from a job, consider using . + public static void DynamicRemoveBulkPoint(this UnsafeTriangulator @this, OutputData output, int pId, Allocator allocator) => new UnsafeTriangulator().DynamicRemoveBulkPoint(output, pId, allocator); /// /// Performs triangulation on the given , producing the result in based on the settings specified in . @@ -1019,6 +1045,19 @@ public static void DynamicInsertPoint(this UnsafeTriangulator @this, Ou /// /// The allocator to use. If called from a job, consider using . public static void DynamicSplitHalfedge(this UnsafeTriangulator @this, OutputData output, int he, float alpha, Allocator allocator) => new UnsafeTriangulator().DynamicSplitHalfedge(UnsafeUtility.As, OutputData>(ref output), he, alpha, allocator); + /// + /// Removes the specified point from the data + /// and re-triangulates the affected region to maintain a valid triangulation. + /// This method supports only the removal of bulk points, i.e., points that are not located on the triangulation boundary. + /// + /// + /// Note: + /// This method requires that contains valid triangulation data. + /// The native containers must be allocated by the user. Some buffers are optional; refer to the documentation for more details. + /// + /// The index of the bulk point to remove. + /// The allocator to use. If called from a job, consider using . + public static void DynamicRemoveBulkPoint(this UnsafeTriangulator @this, OutputData output, int pId, Allocator allocator) => new UnsafeTriangulator().DynamicRemoveBulkPoint(UnsafeUtility.As, OutputData>(ref output), pId, allocator); /// /// Performs triangulation on the given , producing the result in based on the settings specified in . @@ -1097,6 +1136,19 @@ public static void DynamicInsertPoint(this UnsafeTriangulator @this, Ou /// /// The allocator to use. If called from a job, consider using . public static void DynamicSplitHalfedge(this UnsafeTriangulator @this, OutputData output, int he, double alpha, Allocator allocator) => new UnsafeTriangulator().DynamicSplitHalfedge(output, he, alpha, allocator); + /// + /// Removes the specified point from the data + /// and re-triangulates the affected region to maintain a valid triangulation. + /// This method supports only the removal of bulk points, i.e., points that are not located on the triangulation boundary. + /// + /// + /// Note: + /// This method requires that contains valid triangulation data. + /// The native containers must be allocated by the user. Some buffers are optional; refer to the documentation for more details. + /// + /// The index of the bulk point to remove. + /// The allocator to use. If called from a job, consider using . + public static void DynamicRemoveBulkPoint(this UnsafeTriangulator @this, OutputData output, int pId, Allocator allocator) => new UnsafeTriangulator().DynamicRemoveBulkPoint(output, pId, allocator); /// /// Performs triangulation on the given , producing the result in based on the settings specified in . @@ -1198,6 +1250,19 @@ public static void DynamicInsertPoint(this UnsafeTriangulator @this, Output /// /// The allocator to use. If called from a job, consider using . public static void DynamicSplitHalfedge(this UnsafeTriangulator @this, OutputData output, int he, fp alpha, Allocator allocator) => new UnsafeTriangulator().DynamicSplitHalfedge(output, he, alpha, allocator); + /// + /// Removes the specified point from the data + /// and re-triangulates the affected region to maintain a valid triangulation. + /// This method supports only the removal of bulk points, i.e., points that are not located on the triangulation boundary. + /// + /// + /// Note: + /// This method requires that contains valid triangulation data. + /// The native containers must be allocated by the user. Some buffers are optional; refer to the documentation for more details. + /// + /// The index of the bulk point to remove. + /// The allocator to use. If called from a job, consider using . + public static void DynamicRemoveBulkPoint(this UnsafeTriangulator @this, OutputData output, int pId, Allocator allocator) => new UnsafeTriangulator().DynamicRemoveBulkPoint(output, pId, allocator); #endif } @@ -1443,6 +1508,229 @@ public void DynamicSplitHalfedge(OutputData output, int he, T alpha, Allocat } } + public void DynamicRemoveBulkPoint(OutputData output, int pId, Allocator allocator) + { + /// This utility removes the specified point `pId`. It is designed specifically for handling bulk points only! + /// + /// The algorithm operates as follows: + /// + /// 1. Iterate over all triangles containing `pId` to create: + /// loops of halfedges `heLoop`, points `pIdLoop`, and visited triangles. + /// + /// p1 h1 p2 h2 + /// o --------- o + /// .' '. + /// .' '. + /// .' * pId 'o p3 h3 + /// o p0 h0 ..'' + /// .... ....'' + /// '''....o''' + /// p4 h3 + /// + /// heLoop = [h0, h1, h2, h3, h4] + /// pIdLoop = [p0, p1, p2, p3, p4] + /// oheLoop = [h0', h1', h2', h3', h4'], hi' = halfedges[hi] + /// + /// 2. Remove all visited triangles and adapt `oheLoop` to reflect the changes. + /// 3. Triangulate (with restoring boundaries) the cavity created by `pIdLoop` points. + /// 4. Merge the resulting triangulation with the newly created triangulated cavity. + /// 5. Remove `pId` and adjust triangle indexes accordingly. + using var heLoop = new NativeList(allocator); + BuildHeLoop(output, heLoop, pId); + using var visitedTriangles = new NativeArray(output.Triangles.Length / 3, allocator); + using var pIdLoop = new NativeArray(heLoop.Length, allocator); + using var oheLoop = new NativeArray(heLoop.Length, allocator); + using var oheConstrained = new NativeArray(heLoop.Length, allocator); + BuildLoops(output, heLoop, visitedTriangles, pIdLoop, oheLoop, oheConstrained, out var tIdMinVisited); + RemoveTriangles(output, visitedTriangles, tIdMinVisited, oheLoop); + using var cavityTriangles = new NativeList(allocator); + using var cavityHalfedges = new NativeList(allocator); + TriangulateCavity(output, pIdLoop, cavityTriangles, cavityHalfedges, allocator); + MergeTriangulations(output, pIdLoop, cavityTriangles, cavityHalfedges, oheLoop, oheConstrained); + AdaptPoints(output, pId); + } + + private static void BuildHeLoop(OutputData output, NativeList heLoop, int pId) + { + var h0 = -1; + /// NOTE: This can be optimized to an O(1) operation by introducing a `pointToHalfedge` buffer. + /// However, `pointToHalfedge` is currently only utilized during the Sloan algorithm (constraints), + /// and other parts of the code are not yet adapted to incorporate `pointToHalfedge`. + /// That said, having O(n) complexity here is not a significant issue. + for (int i = 0; i < output.Triangles.Length; i++) + { + if (output.Triangles[i] == pId) + { + h0 = i; + break; + } + } + + h0 = NextHalfedge(h0); + var h1 = NextHalfedge(h0); + var p = output.Triangles[h0]; + var q = output.Triangles[h1]; + heLoop.Add(h0); + while (p != q) + { + h0 = NextHalfedge(output.Halfedges[h1]); + h1 = NextHalfedge(h0); + q = output.Triangles[h1]; + heLoop.Add(h0); + } + } + + private static void BuildLoops(OutputData output, NativeList heLoop, NativeArray visitedTriangles, NativeArray pIdLoop, NativeArray oheLoop, NativeArray oheConstrained, out int tIdMinVisited) + { + tIdMinVisited = int.MaxValue; + for (int i = 0; i < heLoop.Length; i++) + { + var he = heLoop[i]; + visitedTriangles[he / 3] = true; + tIdMinVisited = math.min(he / 3, tIdMinVisited); + pIdLoop[i] = output.Triangles[he]; + oheLoop[i] = output.Halfedges[he]; + oheConstrained[i] = output.ConstrainedHalfedges[he]; + } + } + + private static void RemoveTriangles(OutputData output, NativeArray visitedTriangles, int tIdMinVisited, NativeArray halfedgeLoop) + { + static void DisableHe(NativeList halfedges, int he, int rId) + { + var ohe = halfedges[3 * rId + he]; + if (ohe != -1) + { + halfedges[ohe] = -1; + } + } + + static void AdaptHe(NativeList halfedges, int he, int rId, int wId) + { + var ohe = halfedges[3 * rId + he]; + halfedges[3 * wId + he] = ohe; + if (ohe != -1) + { + halfedges[ohe] = 3 * wId + he; + } + } + + var triangles = output.Triangles; + var halfedges = output.Halfedges; + var constrainedHalfedges = output.ConstrainedHalfedges; + + // Reinterpret to a larger struct to make copies of whole triangles slightly more efficient + var constrainedHalfedges3 = constrainedHalfedges.AsArray().Reinterpret(1); + var triangles3 = triangles.AsArray().Reinterpret(4); + + var wId = tIdMinVisited; + for (int rId = tIdMinVisited; rId < triangles3.Length; rId++) + { + if (!visitedTriangles[rId]) + { + triangles3[wId] = triangles3[rId]; + constrainedHalfedges3[wId] = constrainedHalfedges3[rId]; + AdaptHe(halfedges, 0, rId, wId); + AdaptHe(halfedges, 1, rId, wId); + AdaptHe(halfedges, 2, rId, wId); + wId++; + } + else + { + DisableHe(halfedges, 0, rId); + DisableHe(halfedges, 1, rId); + DisableHe(halfedges, 2, rId); + + for (int i = 0; i < halfedgeLoop.Length; i++) + { + var he = halfedgeLoop[i]; + halfedgeLoop[i] = he != -1 && he / 3 > wId ? he - 3 : he; + } + } + } + + // Trim the data to reflect removed triangles. + triangles.Length = 3 * wId; + constrainedHalfedges.Length = 3 * wId; + halfedges.Length = 3 * wId; + } + + private static void TriangulateCavity(OutputData output, NativeArray pIdLoop, NativeList cavityTriangles, NativeList cavityHalfedges, Allocator allocator) + { + using var cavityPositions = new NativeArray(pIdLoop.Length, allocator); + using var cavityConstraints = new NativeArray(2 * pIdLoop.Length, allocator); + + void BuildInput(NativeArray cavityPositions, NativeArray cavityConstraints) + { + for (int i = 0; i < pIdLoop.Length; i++) + { + cavityPositions[i] = output.Positions[pIdLoop[i]]; + } + for (int i = 0; i < pIdLoop.Length - 1; i++) + { + cavityConstraints[2 * i + 0] = i; + cavityConstraints[2 * i + 1] = i + 1; + } + cavityConstraints[^2] = pIdLoop.Length - 1; + cavityConstraints[^1] = 0; + } + + BuildInput(cavityPositions, cavityConstraints); + + new UnsafeTriangulator().Triangulate( + input: new() { Positions = cavityPositions, ConstraintEdges = cavityConstraints }, + output: new() { Triangles = cavityTriangles, Halfedges = cavityHalfedges }, + args: Args.Default(restoreBoundary: true), + allocator + ); + } + + private static void MergeTriangulations(OutputData output, NativeArray pIdLoop, NativeList cavityTriangles, NativeList cavityHalfedges, NativeArray oheLoop, NativeArray oheConstrained) + { + output.ConstrainedHalfedges.Length += cavityHalfedges.Length; + + var ell = output.Triangles.Length; + for (int i = 0; i < cavityHalfedges.Length; i++) + { + var he = cavityHalfedges[i]; + if (he != -1) + { + cavityHalfedges[i] = he + ell; + } + else + { + var id = cavityTriangles[i]; + var ohe = oheLoop[id]; + output.ConstrainedHalfedges[i + ell] |= oheConstrained[id]; + cavityHalfedges[i] = ohe; + if (ohe != -1) + { + output.Halfedges[ohe] = i + ell; + } + } + } + + for (int i = 0; i < cavityTriangles.Length; i++) + { + cavityTriangles[i] = pIdLoop[cavityTriangles[i]]; + } + + output.Triangles.AddRange(cavityTriangles.AsArray()); + output.Halfedges.AddRange(cavityHalfedges.AsArray()); + } + + private static void AdaptPoints(OutputData output, int pId) + { + output.Positions.RemoveAt(pId); + + var triangles = output.Triangles; + for (int i = 0; i < triangles.Length; i++) + { + var t = triangles[i]; + triangles[i] = t > pId ? t - 1 : t; + } + } + private void PreProcessInputStep(InputData input, OutputData output, Args args, out NativeArray localHoles, out TTransform lt, Allocator allocator) { using var _ = Markers.PreProcessInputStep.Auto(); diff --git a/Tests/TestUtils.cs b/Tests/TestUtils.cs index 3756be1..f165f50 100644 --- a/Tests/TestUtils.cs +++ b/Tests/TestUtils.cs @@ -194,6 +194,7 @@ public static void PlantHoleSeeds(this LowLevel.Unsafe.UnsafeTriangulator #endif _ => (dynamic)alpha, }, allocator); + public static void DynamicRemoveBulkPoint(this LowLevel.Unsafe.UnsafeTriangulator triangulator, LowLevel.Unsafe.OutputData output, int pId, Allocator allocator) where T : unmanaged => LowLevel.Unsafe.Extensions.DynamicRemoveBulkPoint((dynamic)triangulator, (dynamic)output, pId, allocator); public static void Run(this Triangulator triangulator) where T : unmanaged => Extensions.Run((dynamic)triangulator); public static JobHandle Schedule(this Triangulator triangulator, JobHandle dependencies = default) where T : unmanaged => diff --git a/Tests/UnsafeTriangulatorEditorTests.cs b/Tests/UnsafeTriangulatorEditorTests.cs index 33a98b0..845783e 100644 --- a/Tests/UnsafeTriangulatorEditorTests.cs +++ b/Tests/UnsafeTriangulatorEditorTests.cs @@ -737,5 +737,189 @@ float len(int he) static int NextHalfedge(int he) => he % 3 == 2 ? he - 2 : he + 1; } + + [Test] + public void RemovePointPartialTriangulationTest() + { + var t = new UnsafeTriangulator(); + + using var inputPositions = new NativeArray(new[] + { + math.double2(0, 0), + math.double2(3, 0), + math.double2(3, 3), + math.double2(0, 3), + math.double2(1, 0.5f), + math.double2(2, 0.5f), + math.double2(2.5f, 1.5f), + math.double2(1.5f, 2.5f), + math.double2(0.5f, 1.5f), + math.double2(1.5f, 1.5f), + }.DynamicCast(), Allocator.Persistent); + + using var outputPositions = new NativeList(Allocator.Persistent); + using var triangles = new NativeList(Allocator.Persistent); + using var constrainedHalfedges = new NativeList(Allocator.Persistent); + using var halfedges = new NativeList(Allocator.Persistent); + t.Triangulate( + input: new() { Positions = inputPositions }, + output: new() { Positions = outputPositions, Triangles = triangles, ConstrainedHalfedges = constrainedHalfedges, Halfedges = halfedges }, + args: Args.Default(), + allocator: Allocator.Persistent + ); + + TestUtils.Draw(outputPositions.AsReadOnly().CastToFloat2(), triangles.AsReadOnly(), Color.blue, 5f); + + var constrain = constrainedHalfedges.AsArray(); + for (int i = 0; i < triangles.Length; i++) + { + var p = triangles[i]; + var q = triangles[NextHalfedge(i)]; + static int NextHalfedge(int he) => he % 3 == 2 ? he - 2 : he + 1; + if (p == 5 && q == 6) + { + constrain[i] = true; + constrain[halfedges[i]] = true; + break; + } + } + + t.DynamicRemoveBulkPoint( + output: new() { Positions = outputPositions, Triangles = triangles, ConstrainedHalfedges = constrainedHalfedges, Halfedges = halfedges }, + pId: 9, + allocator: Allocator.Persistent + ); + + TestUtils.Draw(outputPositions.AsReadOnly().CastToFloat2(), triangles.AsReadOnly(), Color.red, 5f); + + Assert.That(outputPositions.AsReadOnly(), Is.EqualTo(inputPositions.AsReadOnly().ToArray()[..^1])); + Assert.That(triangles.AsReadOnly(), Is.EqualTo( + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 + new[] { 6, 1, 5, 5, 1, 4, 7, 2, 6, 6, 2, 1, 1, 0, 4, 4, 0, 8, 8, 3, 7, 7, 3, 2, 0, 3, 8, 6, 5, 4, 4, 8, 6, 8, 7, 6, } + )); + Assert.That(halfedges.AsReadOnly(), Is.EqualTo( + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 + new[] { 11, 3, 27, 1, 14, 28, 23, 9, 34, 7, -1, 0, -1, 15, 4, 13, 26, 30, 25, 21, 33, 19, -1, 6, -1, 18, 16, 2, 5, 32, 17, 35, 29, 20, 8, 31, } + )); + var expectedConstrained = new bool[36]; + expectedConstrained[2] = true; + expectedConstrained[27] = true; + Assert.That(constrainedHalfedges.AsReadOnly(), Is.EqualTo(expectedConstrained)); + } + + [Test] + public void RemovePointFullTriangulationTest() + { + var t = new UnsafeTriangulator(); + + using var inputPositions = new NativeArray(new[] + { + math.double2(0, 0), + math.double2(1, 0), + math.double2(2, 1), + math.double2(2, 2), + math.double2(1, 3), + math.double2(0, 3), + math.double2(-1, 2), + math.double2(-1, 1), + math.double2(1, 1), + }.DynamicCast(), Allocator.Persistent); + + using var outputPositions = new NativeList(Allocator.Persistent); + using var triangles = new NativeList(Allocator.Persistent); + using var constrainedHalfedges = new NativeList(Allocator.Persistent); + using var halfedges = new NativeList(Allocator.Persistent); + t.Triangulate( + input: new() { Positions = inputPositions }, + output: new() { Positions = outputPositions, Triangles = triangles, ConstrainedHalfedges = constrainedHalfedges, Halfedges = halfedges }, + args: Args.Default(), + allocator: Allocator.Persistent + ); + + TestUtils.Draw(outputPositions.AsReadOnly().CastToFloat2(), triangles.AsReadOnly(), Color.blue, 5f); + + var constrain = constrainedHalfedges.AsArray(); + for (int i = 0; i < triangles.Length; i++) + { + var p = triangles[i]; + var q = triangles[NextHalfedge(i)]; + static int NextHalfedge(int he) => he % 3 == 2 ? he - 2 : he + 1; + if (p == 1 && q == 0) + { + constrain[i] = true; + break; + } + } + + t.DynamicRemoveBulkPoint( + output: new() { Positions = outputPositions, Triangles = triangles, ConstrainedHalfedges = constrainedHalfedges, Halfedges = halfedges }, + pId: 8, + allocator: Allocator.Persistent + ); + + TestUtils.Draw(outputPositions.AsReadOnly().CastToFloat2(), triangles.AsReadOnly(), Color.red, 5f); + + Assert.That(outputPositions.AsReadOnly(), Is.EqualTo(inputPositions.AsReadOnly().ToArray()[..^1])); + Assert.That(triangles.AsReadOnly(), Is.EqualTo( + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + new[] { 1, 0, 7, 7, 6, 1, 6, 5, 1, 5, 4, 1, 4, 3, 1, 3, 2, 1, } + )); + Assert.That(halfedges.AsReadOnly(), Is.EqualTo( + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + new[] { -1, -1, 5, -1, 8, 2, -1, 11, 4, -1, 14, 7, -1, 17, 10, -1, -1, 13, } + )); + Assert.That(constrainedHalfedges.AsReadOnly(), Is.EqualTo(new[] { + true, false, false, + false, false, false, false, false, + false, false, false, false, false, + false, false, false, false, false, + })); + } + + [Test] + public void RemovePointFromMiddleIndexTest() + { + var t = new UnsafeTriangulator(); + + using var inputPositions = new NativeArray(new[] + { + math.double2(0, 0), + math.double2(1, 0), + math.double2(0.5f, 1), + math.double2(1.5f, 1.5f), + math.double2(0.5f, 2f), + math.double2(-0.5f, 1.5f), + }.DynamicCast(), Allocator.Persistent); + + using var outputPositions = new NativeList(Allocator.Persistent); + using var triangles = new NativeList(Allocator.Persistent); + using var constrainedHalfedges = new NativeList(Allocator.Persistent); + using var halfedges = new NativeList(Allocator.Persistent); + t.Triangulate( + input: new() { Positions = inputPositions }, + output: new() { Positions = outputPositions, Triangles = triangles, ConstrainedHalfedges = constrainedHalfedges, Halfedges = halfedges }, + args: Args.Default(), + allocator: Allocator.Persistent + ); + + TestUtils.Draw(outputPositions.AsReadOnly().CastToFloat2(), triangles.AsReadOnly(), Color.blue, 5f); + + t.DynamicRemoveBulkPoint( + output: new() { Positions = outputPositions, Triangles = triangles, ConstrainedHalfedges = constrainedHalfedges, Halfedges = halfedges }, + pId: 2, + allocator: Allocator.Persistent + ); + + TestUtils.Draw(outputPositions.AsReadOnly().CastToFloat2(), triangles.AsReadOnly(), Color.red, 5f); + + Assert.That(triangles.AsReadOnly(), Is.EqualTo( + // 0 1 2 3 4 5 6 7 8 + new[] { 3, 2, 1, 1, 0, 3, 0, 4, 3, } + )); + Assert.That(halfedges.AsReadOnly(), Is.EqualTo( + // 0 1 2 3 4 5 6 7 8 + new[] { -1, -1, 5, -1, 8, 2, -1, -1, 4, } + )); + } } }