A game framework written with osu! in mind.

Implement polygon clipping

+366
+199
osu.Framework.Tests/Polygons/ConvexPolygonClipping.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using NUnit.Framework; 6 + using osu.Framework.Extensions.PolygonExtensions; 7 + using osu.Framework.Graphics.Primitives; 8 + using osuTK; 9 + 10 + namespace osu.Framework.Tests.Polygons 11 + { 12 + [TestFixture] 13 + public class ConvexPolygonClipping 14 + { 15 + private static readonly Vector2 origin = Vector2.Zero; 16 + private static readonly Vector2 up_1 = new Vector2(0, 1); 17 + private static readonly Vector2 up_2 = new Vector2(0, 2); 18 + private static readonly Vector2 up_3 = new Vector2(0, 3); 19 + private static readonly Vector2 down_1 = new Vector2(0, -1); 20 + private static readonly Vector2 down_2 = new Vector2(0, -2); 21 + private static readonly Vector2 down_3 = new Vector2(0, -3); 22 + private static readonly Vector2 left_1 = new Vector2(-1, 0); 23 + private static readonly Vector2 left_2 = new Vector2(-2, 0); 24 + private static readonly Vector2 left_3 = new Vector2(-3, 0); 25 + private static readonly Vector2 right_1 = new Vector2(1, 0); 26 + private static readonly Vector2 right_2 = new Vector2(2, 0); 27 + private static readonly Vector2 right_3 = new Vector2(3, 0); 28 + 29 + private static object[] externalTestCases => new object[] 30 + { 31 + // Non-rotated 32 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { up_2, up_3, up_3 + right_1, up_2 } }, 33 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { right_2, right_2 + up_1, right_3 + up_1, right_3 } }, 34 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { down_1, down_1 + right_1, down_2 + right_1, down_2 } }, 35 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { left_2, left_2 + up_1, left_1 + up_1, left_1 } }, 36 + // Rotated 37 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { up_1 + right_2, up_2 + right_1, up_3 + right_2, up_2 + right_3 } }, 38 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { up_1 + right_2, down_1 + right_3, down_2 + right_2, down_1 + right_1 } }, 39 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { down_1 + right_1, down_2 + right_2, down_3 + right_1, down_2 } }, 40 + new object[] { new[] { origin, up_1, up_1 + right_1, right_1 }, new[] { left_1 + up_1, down_2, down_3 + left_2, left_2 } }, 41 + }; 42 + 43 + [TestCaseSource(nameof(externalTestCases))] 44 + public void TestExternalPolygon(Vector2[] polygonVertices1, Vector2[] polygonVertices2) 45 + { 46 + var poly1 = new SimpleConvexPolygon(polygonVertices1); 47 + var poly2 = new SimpleConvexPolygon(polygonVertices2); 48 + 49 + Assert.That(poly1.Clip(poly2).Length, Is.Zero); 50 + Assert.That(poly2.Clip(poly1).Length, Is.Zero); 51 + 52 + Array.Reverse(polygonVertices1); 53 + Array.Reverse(polygonVertices2); 54 + 55 + Assert.That(poly1.Clip(poly2).Length, Is.Zero); 56 + Assert.That(poly2.Clip(poly1).Length, Is.Zero); 57 + } 58 + 59 + private static object[] subjectFullyContainedTestCases => new object[] 60 + { 61 + // Same polygon 62 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { origin, up_2, up_2 + right_2, right_2 } }, 63 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { down_2, left_2, up_2, right_2 } }, 64 + // Corners 65 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { origin, up_1, up_1 + right_1, right_1 } }, 66 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { up_1, up_2, up_2 + right_1, up_1 + right_1 } }, 67 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { up_1 + right_1, up_2 + right_1, up_2 + right_2, up_1 + right_2 } }, 68 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { right_1, up_1 + right_1, up_1 + right_2, right_2 } }, 69 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { down_2, down_1, right_1, right_2 } }, 70 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { left_2, left_1, down_1, down_2 } }, 71 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { left_2, up_2, up_1, left_1 } }, 72 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { up_2, right_2, right_1, up_1 } }, 73 + // Padded 74 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { right_1 * 0.5f + up_1 * 0.5f, up_2 * 0.75f, up_2 * 0.75f + right_2 * 0.75f, right_2 * 0.75f } }, 75 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { down_1, left_1, up_1, right_1 } }, 76 + // Rotated 77 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { up_1 + right_1 * 0.5f, up_2 * 0.75f + right_1, up_1 + right_2 * 0.5f, up_1 * 0.5f + right_1 } }, 78 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { up_1, up_2 * 0.75f, up_2 + right_1 * 0.5f, up_2 + right_1 } }, 79 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { right_1, up_1 + right_2, up_1 * 0.5f + right_2, right_2 * 0.75f } }, 80 + new object[] { new[] { down_2, left_2, up_2, right_2 }, new[] { left_1 + up_1, up_1 + right_1, down_1 + right_1, left_1 + down_1 } }, 81 + }; 82 + 83 + [TestCaseSource(nameof(subjectFullyContainedTestCases))] 84 + public void TestSubjectFullyContained(Vector2[] clipVertices, Vector2[] subjectVertices) 85 + { 86 + var clipPolygon = new SimpleConvexPolygon(clipVertices); 87 + var subjectPolygon = new SimpleConvexPolygon(subjectVertices); 88 + 89 + assertPolygonEquals(subjectPolygon, new SimpleConvexPolygon(clipPolygon.Clip(subjectPolygon).ToArray()), false); 90 + 91 + Array.Reverse(clipVertices); 92 + Array.Reverse(subjectVertices); 93 + 94 + assertPolygonEquals(subjectPolygon, new SimpleConvexPolygon(clipPolygon.Clip(subjectPolygon).ToArray()), true); 95 + } 96 + 97 + [TestCaseSource(nameof(subjectFullyContainedTestCases))] 98 + public void TestClipFullyContained(Vector2[] subjectVertices, Vector2[] clipVertices) 99 + { 100 + var clipPolygon = new SimpleConvexPolygon(clipVertices); 101 + var subjectPolygon = new SimpleConvexPolygon(subjectVertices); 102 + 103 + assertPolygonEquals(clipPolygon, new SimpleConvexPolygon(clipPolygon.Clip(subjectPolygon).ToArray()), false); 104 + 105 + Array.Reverse(clipVertices); 106 + Array.Reverse(subjectVertices); 107 + 108 + assertPolygonEquals(clipPolygon, new SimpleConvexPolygon(clipPolygon.Clip(subjectPolygon).ToArray()), true); 109 + } 110 + 111 + private static object[] generalClippingTestCases => new object[] 112 + { 113 + new object[] 114 + { 115 + new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { left_1 + up_1, up_1 + right_1, down_1 + right_1, left_1 + down_1 }, new[] { origin, up_1, up_1 + right_1, right_1 } 116 + }, 117 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { left_1, up_1, right_1, down_1 }, new[] { origin, up_1, right_1 } }, 118 + new object[] 119 + { 120 + new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { up_1 + right_1, up_3 + right_1, up_3 + right_3, up_1 + right_3 }, 121 + new[] { up_1 + right_1, up_2 + right_1, up_2 + right_2, up_1 + right_2 } 122 + }, 123 + new object[] 124 + { 125 + new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { up_2 + right_1, up_3 + right_2, up_2 + right_3, up_1 + right_2 }, new[] { up_2 + right_1, up_2 + right_2, up_1 + right_2 } 126 + }, 127 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { left_1 + up_1, up_3 + right_1, up_1 + right_1, origin }, new[] { up_2, up_2 + right_1, up_1 + right_1, origin } }, 128 + new object[] 129 + { 130 + new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { left_1 + up_1, up_1 + right_3, down_1 + right_3, left_1 + down_1 }, new[] { up_1, up_1 + right_2, right_2, origin } 131 + }, 132 + new object[] 133 + { 134 + new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { down_1 + left_1, up_3 + left_1, up_3 + right_1, down_1 + right_1 }, new[] { origin, up_2, up_2 + right_1, right_1 } 135 + }, 136 + new object[] 137 + { 138 + new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { down_1, up_1 + right_1, down_1 + right_2, down_2 + right_1 }, new[] { right_1 * 0.5f, up_1 + right_1, right_2 * 0.75f } 139 + }, 140 + new object[] { new[] { origin, up_2, up_2 + right_2, right_2 }, new[] { origin, up_3 + right_3, right_2 }, new[] { origin, up_2 + right_2, right_2 } }, 141 + new object[] { new[] { up_2, right_2, down_2, left_2 }, new[] { left_1 + down_2, left_2 + down_2, up_2 + left_2, up_2 + left_1 }, new[] { up_1 + left_1, down_1 + left_1, left_2 } }, 142 + new object[] { new[] { up_2, right_2, down_2, left_2 }, new[] { origin, down_2 + left_2, up_2 + left_2 }, new[] { origin, down_1 + left_1, left_2, up_1 + left_1 } }, 143 + new object[] { new[] { up_2, right_2, down_2, left_2 }, new[] { origin, left_3, up_3 }, new[] { origin, left_2, up_2 } }, 144 + new object[] { new[] { up_2, right_2, down_2, left_2 }, new[] { origin, left_3, up_3, right_3 }, new[] { origin, left_2, up_2, right_2 } }, 145 + new object[] 146 + { 147 + new[] { left_1 + up_1, right_1 + up_1, down_1 + right_1, down_1 + left_1 }, new[] { up_2 * 0.75f, right_2 * 0.75f, down_2 * 0.75f, left_2 * 0.75f }, 148 + new[] 149 + { 150 + down_1 + left_1 * 0.5f, 151 + down_1 * 0.5f + left_1, 152 + up_1 * 0.5f + left_1, 153 + up_1 + left_1 * 0.5f, 154 + up_1 + right_1 * 0.5f, 155 + up_1 * 0.5f + right_1, 156 + down_1 * 0.5f + right_1, 157 + down_1 + right_1 * 0.5f 158 + } 159 + }, 160 + new object[] 161 + { 162 + new[] { up_1, right_1, left_1 }, new[] { up_1 + right_1 * 0.5f, down_1 + right_1 * 0.5f, down_1 + left_1 * 0.5f, up_1 + left_1 * 0.5f }, 163 + new[] { up_1 * 0.5f + left_1 * 0.5f, up_1, up_1 * 0.5f + right_1 * 0.5f, right_1 * 0.5f, left_1 * 0.5f, } 164 + }, 165 + new object[] 166 + { 167 + // Inverse of the above 168 + new[] { up_1 + right_1 * 0.5f, down_1 + right_1 * 0.5f, down_1 + left_1 * 0.5f, up_1 + left_1 * 0.5f }, new[] { up_1, right_1, left_1 }, 169 + new[] { up_1 * 0.5f + left_1 * 0.5f, up_1, up_1 * 0.5f + right_1 * 0.5f, right_1 * 0.5f, left_1 * 0.5f, } 170 + }, 171 + new object[] 172 + { 173 + new[] { up_1, right_1, down_1 + right_1, down_1 + left_1, left_1 }, new[] { left_1, up_1, up_1 + right_1, down_1 + right_1, down_1 }, 174 + new[] { up_1, right_1, right_1 + down_1, down_1, left_1 } 175 + } 176 + }; 177 + 178 + [TestCaseSource(nameof(generalClippingTestCases))] 179 + public void TestGeneralClipping(Vector2[] clipVertices, Vector2[] subjectVertices, Vector2[] resultingVertices) 180 + { 181 + var clipPolygon = new SimpleConvexPolygon(clipVertices); 182 + var subjectPolygon = new SimpleConvexPolygon(subjectVertices); 183 + 184 + assertPolygonEquals(new SimpleConvexPolygon(resultingVertices), new SimpleConvexPolygon(clipPolygon.Clip(subjectPolygon).ToArray()), false); 185 + 186 + Array.Reverse(clipVertices); 187 + Array.Reverse(subjectVertices); 188 + 189 + // The expected polygon is never reversed 190 + assertPolygonEquals(new SimpleConvexPolygon(resultingVertices), new SimpleConvexPolygon(clipPolygon.Clip(subjectPolygon).ToArray()), false); 191 + } 192 + 193 + private void assertPolygonEquals(IPolygon expected, IPolygon actual, bool reverse) 194 + => Assert.That(PolygonExtensions.GetRotation(actual.GetVertices()), 195 + reverse 196 + ? Is.EqualTo(-PolygonExtensions.GetRotation(expected.GetVertices())) 197 + : Is.EqualTo(PolygonExtensions.GetRotation(expected.GetVertices()))); 198 + } 199 + }
+143
osu.Framework/Extensions/PolygonExtensions/PolygonExtensions.cs
··· 63 63 64 64 return buffer.Slice(0, vertices.Length); 65 65 } 66 + 67 + public static int GetClipBufferSize<TPolygon1, TPolygon2>(this TPolygon1 clipPolygon, TPolygon2 subjectPolygon) 68 + where TPolygon1 : IPolygon 69 + where TPolygon2 : IPolygon 70 + { 71 + if (clipPolygon is IConvexPolygon && subjectPolygon is IConvexPolygon) 72 + { 73 + // If both polygons are convex, there can only be at most 2 intersections for each edge of the subject 74 + return subjectPolygon.GetVertices().Length * 2; 75 + } 76 + 77 + // If both polygons are non-convex, each edge of one may intersect with each edge of the other, leading to at most n^2 vertices 78 + // For simplicity, the case where only one of the two is non-convex is also covered under this case 79 + return clipPolygon.GetVertices().Length * subjectPolygon.GetVertices().Length; 80 + } 81 + 82 + public static Span<Vector2> Clip<TPolygon1, TPolygon2>(this TPolygon1 clipPolygon, TPolygon2 subjectPolygon) 83 + where TPolygon1 : IPolygon 84 + where TPolygon2 : IPolygon 85 + => clipPolygon.Clip(subjectPolygon, new Vector2[clipPolygon.GetClipBufferSize(subjectPolygon)]); 86 + 87 + public static Span<Vector2> Clip<TPolygon1, TPolygon2>(this TPolygon1 clipPolygon, TPolygon2 subjectPolygon, Span<Vector2> buffer) 88 + where TPolygon1 : IPolygon 89 + where TPolygon2 : IPolygon 90 + { 91 + if (buffer.Length < GetClipBufferSize(clipPolygon, subjectPolygon)) 92 + { 93 + throw new ArgumentException($"Clip buffer must have a length of {GetClipBufferSize(clipPolygon, subjectPolygon)}, but was {buffer.Length}." 94 + + $"Use {nameof(GetClipBufferSize)} to calculate the size of the buffer.", nameof(buffer)); 95 + } 96 + 97 + ReadOnlySpan<Vector2> subjectVertices = subjectPolygon.GetVertices(); 98 + ReadOnlySpan<Vector2> clipVertices = clipPolygon.GetVertices(); 99 + 100 + // Buffer is initially filled with the all of the subject's vertices 101 + subjectVertices.CopyTo(buffer); 102 + 103 + // Make sure that the subject vertices are clockwise-sorted 104 + ClockwiseSort(buffer.Slice(0, subjectVertices.Length)); 105 + 106 + // The edges of clip that the subject will be clipped against 107 + Span<Line> clipEdges = stackalloc Line[clipVertices.Length]; 108 + 109 + // Joins consecutive vertices to form the clip edges 110 + // This is done via GetRotation() to avoid a secondary temporary storage 111 + if (GetRotation(clipVertices) < 0) 112 + { 113 + for (int i = clipVertices.Length - 1, c = 0; i > 0; i--, c++) 114 + clipEdges[c] = new Line(clipVertices[i], clipVertices[i - 1]); 115 + clipEdges[clipEdges.Length - 1] = new Line(clipVertices[0], clipVertices[clipVertices.Length - 1]); 116 + } 117 + else 118 + { 119 + for (int i = 0; i < clipVertices.Length - 1; i++) 120 + clipEdges[i] = new Line(clipVertices[i], clipVertices[i + 1]); 121 + clipEdges[clipEdges.Length - 1] = new Line(clipVertices[clipVertices.Length - 1], clipVertices[0]); 122 + } 123 + 124 + // Number of vertices in the buffer that need to be tested against 125 + // This becomes the number of vertices in the resulting polygon after each clipping iteration 126 + int inputCount = subjectVertices.Length; 127 + 128 + // Temporary storage for the vertices from the buffer as the buffer gets altered 129 + Span<Vector2> inputVertices = stackalloc Vector2[buffer.Length]; 130 + 131 + foreach (var ce in clipEdges) 132 + { 133 + if (inputCount == 0) 134 + break; 135 + 136 + // Store the original vertices (buffer will get altered) 137 + buffer.CopyTo(inputVertices); 138 + 139 + int outputCount = 0; 140 + var startPoint = inputVertices[inputCount - 1]; 141 + 142 + for (int i = 0; i < inputCount; i++) 143 + { 144 + var endPoint = inputVertices[i]; 145 + 146 + if (isInsideRightHalfPlane(ce, endPoint)) 147 + { 148 + if (!isInsideRightHalfPlane(ce, startPoint)) 149 + buffer[outputCount++] = ce.At(ce.IntersectWith(new Line(startPoint, endPoint)).distance); 150 + 151 + buffer[outputCount++] = endPoint; 152 + } 153 + else if (isInsideRightHalfPlane(ce, startPoint)) 154 + buffer[outputCount++] = ce.At(ce.IntersectWith(new Line(startPoint, endPoint)).distance); 155 + 156 + startPoint = endPoint; 157 + } 158 + 159 + inputCount = outputCount; 160 + } 161 + 162 + return buffer.Slice(0, inputCount); 163 + } 164 + 165 + /// <summary> 166 + /// Determines whether a point is within the right half-plane of a line. 167 + /// </summary> 168 + /// <param name="line">The line.</param> 169 + /// <param name="point">The point.</param> 170 + /// <returns>Whether <paramref name="point"/> is in the right half-plane of <paramref name="line"/>.</returns> 171 + private static bool isInsideRightHalfPlane(Line line, Vector2 point) 172 + { 173 + var diff1 = line.Direction; 174 + var diff2 = point - line.StartPoint; 175 + 176 + return diff1.X * diff2.Y - diff1.Y * diff2.X <= 0; 177 + } 178 + 179 + /// <summary> 180 + /// Retrieves the rotation of a set of vertices. 181 + /// </summary> 182 + /// <param name="vertices">The vertices.</param> 183 + /// <returns>Twice the area enclosed by the vertices. The vertices are in clockwise order if the value is positive.</returns> 184 + public static float GetRotation(ReadOnlySpan<Vector2> vertices) 185 + { 186 + float rotation = 0; 187 + for (int i = 0; i < vertices.Length - 1; ++i) 188 + { 189 + var vi = vertices[i]; 190 + var vj = vertices[i + 1]; 191 + 192 + rotation += (vj.X - vi.X) * (vj.Y + vi.Y); 193 + } 194 + 195 + rotation += (vertices[0].X - vertices[vertices.Length - 1].X) * (vertices[0].Y + vertices[vertices.Length - 1].Y); 196 + 197 + return rotation; 198 + } 199 + 200 + /// <summary> 201 + /// Sorts a set of vertices in clockwise order. 202 + /// </summary> 203 + /// <param name="vertices">The vertices to sort.</param> 204 + public static void ClockwiseSort(Span<Vector2> vertices) 205 + { 206 + if (GetRotation(vertices) < 0) 207 + vertices.Reverse(); 208 + } 66 209 } 67 210 }
+24
osu.Framework/Graphics/Primitives/SimpleConvexPolygon.cs
··· 1 + // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2 + // See the LICENCE file in the repository root for full licence text. 3 + 4 + using System; 5 + using osuTK; 6 + 7 + namespace osu.Framework.Graphics.Primitives 8 + { 9 + public class SimpleConvexPolygon : IConvexPolygon 10 + { 11 + private readonly Vector2[] vertices; 12 + 13 + public SimpleConvexPolygon(Vector2[] vertices) 14 + { 15 + this.vertices = vertices; 16 + } 17 + 18 + public ReadOnlySpan<Vector2> GetAxisVertices() => vertices; 19 + 20 + public ReadOnlySpan<Vector2> GetVertices() => vertices; 21 + 22 + public int MaxClipVertices => vertices.Length * 2; 23 + } 24 + }