A game framework written with osu! in mind.
at master 461 lines 21 kB view raw
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 4using System; 5using System.Collections.Generic; 6using System.Linq; 7using osu.Framework.Graphics.Primitives; 8using osuTK; 9 10namespace osu.Framework.Utils 11{ 12 /// <summary> 13 /// Helper methods to approximate a path by interpolating a sequence of control points. 14 /// </summary> 15 public static class PathApproximator 16 { 17 private const float bezier_tolerance = 0.25f; 18 19 /// <summary> 20 /// The amount of pieces to calculate for each control point quadruplet. 21 /// </summary> 22 private const int catmull_detail = 50; 23 24 private const float circular_arc_tolerance = 0.1f; 25 26 /// <summary> 27 /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing 28 /// the control points until their approximation error vanishes below a given threshold. 29 /// </summary> 30 /// <param name="controlPoints">The control points.</param> 31 /// <returns>A list of vectors representing the piecewise-linear approximation.</returns> 32 public static List<Vector2> ApproximateBezier(ReadOnlySpan<Vector2> controlPoints) 33 { 34 return ApproximateBSpline(controlPoints); 35 } 36 37 /// <summary> 38 /// Creates a piecewise-linear approximation of a clamped uniform B-spline with polynomial order p, 39 /// by dividing it into a series of bezier control points at its knots, then adaptively repeatedly 40 /// subdividing those until their approximation error vanishes below a given threshold. 41 /// Retains previous bezier approximation functionality when p is 0 or too large to create knots. 42 /// Algorithm unsuitable for large values of p with many knots. 43 /// </summary> 44 /// <param name="controlPoints">The control points.</param> 45 /// <param name="p">The polynomial order.</param> 46 /// <returns>A list of vectors representing the piecewise-linear approximation.</returns> 47 public static List<Vector2> ApproximateBSpline(ReadOnlySpan<Vector2> controlPoints, int p = 0) 48 { 49 List<Vector2> output = new List<Vector2>(); 50 int n = controlPoints.Length - 1; 51 52 if (n < 0) 53 return output; 54 55 Stack<Vector2[]> toFlatten = new Stack<Vector2[]>(); 56 Stack<Vector2[]> freeBuffers = new Stack<Vector2[]>(); 57 58 var points = controlPoints.ToArray(); 59 60 if (p > 0 && p < n) 61 { 62 // Subdivide B-spline into bezier control points at knots. 63 for (int i = 0; i < n - p; i++) 64 { 65 var subBezier = new Vector2[p + 1]; 66 subBezier[0] = points[i]; 67 68 // Destructively insert the knot p-1 times via Boehm's algorithm. 69 for (int j = 0; j < p - 1; j++) 70 { 71 subBezier[j + 1] = points[i + 1]; 72 73 for (int k = 1; k < p - j; k++) 74 { 75 int l = Math.Min(k, n - p - i); 76 points[i + k] = (l * points[i + k] + points[i + k + 1]) / (l + 1); 77 } 78 } 79 80 subBezier[p] = points[i + 1]; 81 toFlatten.Push(subBezier); 82 } 83 84 toFlatten.Push(points[(n - p)..]); 85 // Reverse the stack so elements can be accessed in order. 86 toFlatten = new Stack<Vector2[]>(toFlatten); 87 } 88 else 89 { 90 // B-spline subdivision unnecessary, degenerate to single bezier. 91 p = n; 92 toFlatten.Push(points); 93 } 94 // "toFlatten" contains all the curves which are not yet approximated well enough. 95 // We use a stack to emulate recursion without the risk of running into a stack overflow. 96 // (More specifically, we iteratively and adaptively refine our curve with a 97 // <a href="https://en.wikipedia.org/wiki/Depth-first_search">Depth-first search</a> 98 // over the tree resulting from the subdivisions we make.) 99 100 var subdivisionBuffer1 = new Vector2[p + 1]; 101 var subdivisionBuffer2 = new Vector2[p * 2 + 1]; 102 103 Vector2[] leftChild = subdivisionBuffer2; 104 105 while (toFlatten.Count > 0) 106 { 107 Vector2[] parent = toFlatten.Pop(); 108 109 if (bezierIsFlatEnough(parent)) 110 { 111 // If the control points we currently operate on are sufficiently "flat", we use 112 // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation 113 // of the bezier curve represented by our control points, consisting of the same amount 114 // of points as there are control points. 115 bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, p + 1); 116 117 freeBuffers.Push(parent); 118 continue; 119 } 120 121 // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep 122 // subdividing the curve we are currently operating on. 123 Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[p + 1]; 124 bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, p + 1); 125 126 // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration. 127 for (int i = 0; i < p + 1; ++i) 128 parent[i] = leftChild[i]; 129 130 toFlatten.Push(rightChild); 131 toFlatten.Push(parent); 132 } 133 134 output.Add(controlPoints[n]); 135 return output; 136 } 137 138 /// <summary> 139 /// Creates a piecewise-linear approximation of a Catmull-Rom spline. 140 /// </summary> 141 /// <returns>A list of vectors representing the piecewise-linear approximation.</returns> 142 public static List<Vector2> ApproximateCatmull(ReadOnlySpan<Vector2> controlPoints) 143 { 144 var result = new List<Vector2>((controlPoints.Length - 1) * catmull_detail * 2); 145 146 for (int i = 0; i < controlPoints.Length - 1; i++) 147 { 148 var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i]; 149 var v2 = controlPoints[i]; 150 var v3 = i < controlPoints.Length - 1 ? controlPoints[i + 1] : v2 + v2 - v1; 151 var v4 = i < controlPoints.Length - 2 ? controlPoints[i + 2] : v3 + v3 - v2; 152 153 for (int c = 0; c < catmull_detail; c++) 154 { 155 result.Add(catmullFindPoint(ref v1, ref v2, ref v3, ref v4, (float)c / catmull_detail)); 156 result.Add(catmullFindPoint(ref v1, ref v2, ref v3, ref v4, (float)(c + 1) / catmull_detail)); 157 } 158 } 159 160 return result; 161 } 162 163 /// <summary> 164 /// Creates a piecewise-linear approximation of a circular arc curve. 165 /// </summary> 166 /// <returns>A list of vectors representing the piecewise-linear approximation.</returns> 167 public static List<Vector2> ApproximateCircularArc(ReadOnlySpan<Vector2> controlPoints) 168 { 169 CircularArcProperties pr = circularArcProperties(controlPoints); 170 if (!pr.IsValid) 171 return ApproximateBezier(controlPoints); 172 173 // We select the amount of points for the approximation by requiring the discrete curvature 174 // to be smaller than the provided tolerance. The exact angle required to meet the tolerance 175 // is: 2 * Math.Acos(1 - TOLERANCE / r) 176 // The special case is required for extremely short sliders where the radius is smaller than 177 // the tolerance. This is a pathological rather than a realistic case. 178 int amountPoints = 2 * pr.Radius <= circular_arc_tolerance ? 2 : Math.Max(2, (int)Math.Ceiling(pr.ThetaRange / (2 * Math.Acos(1 - circular_arc_tolerance / pr.Radius)))); 179 180 List<Vector2> output = new List<Vector2>(amountPoints); 181 182 for (int i = 0; i < amountPoints; ++i) 183 { 184 double fract = (double)i / (amountPoints - 1); 185 double theta = pr.ThetaStart + pr.Direction * fract * pr.ThetaRange; 186 Vector2 o = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * pr.Radius; 187 output.Add(pr.Centre + o); 188 } 189 190 return output; 191 } 192 193 /// <summary> 194 /// Computes the bounding box of a circular arc. 195 /// </summary> 196 /// <param name="controlPoints">Three distinct points on the arc.</param> 197 /// <returns>The rectangle inscribing the circular arc.</returns> 198 public static RectangleF CircularArcBoundingBox(ReadOnlySpan<Vector2> controlPoints) 199 { 200 CircularArcProperties pr = circularArcProperties(controlPoints); 201 if (!pr.IsValid) 202 return RectangleF.Empty; 203 204 // We find the bounding box using the end-points, as well as 205 // each 90 degree angle inside the range of the arc 206 List<Vector2> points = new List<Vector2> 207 { 208 controlPoints[0], 209 controlPoints[2] 210 }; 211 212 const double right_angle = Math.PI / 2; 213 double step = right_angle * pr.Direction; 214 215 double quotient = pr.ThetaStart / right_angle; 216 // choose an initial right angle, closest to ThetaStart, going in the direction of the arc. 217 // thanks to this, when looping over quadrant points to check if they lie on the arc, we only need to check against ThetaEnd. 218 double closestRightAngle = right_angle * (pr.Direction > 0 ? Math.Ceiling(quotient) : Math.Floor(quotient)); 219 220 // at most, four quadrant points must be considered. 221 for (int i = 0; i < 4; ++i) 222 { 223 double angle = closestRightAngle + step * i; 224 225 // check whether angle has exceeded ThetaEnd. 226 // multiplying by Direction eliminates branching caused by the fact that step can be either positive or negative. 227 if (Precision.DefinitelyBigger((angle - pr.ThetaEnd) * pr.Direction, 0)) 228 break; 229 230 Vector2 o = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * pr.Radius; 231 points.Add(pr.Centre + o); 232 } 233 234 float minX = points.Min(p => p.X); 235 float minY = points.Min(p => p.Y); 236 float maxX = points.Max(p => p.X); 237 float maxY = points.Max(p => p.Y); 238 239 return new RectangleF(minX, minY, maxX - minX, maxY - minY); 240 } 241 242 /// <summary> 243 /// Creates a piecewise-linear approximation of a linear curve. 244 /// Basically, returns the input. 245 /// </summary> 246 /// <returns>A list of vectors representing the piecewise-linear approximation.</returns> 247 public static List<Vector2> ApproximateLinear(ReadOnlySpan<Vector2> controlPoints) 248 { 249 var result = new List<Vector2>(controlPoints.Length); 250 251 foreach (var c in controlPoints) 252 result.Add(c); 253 254 return result; 255 } 256 257 /// <summary> 258 /// Creates a piecewise-linear approximation of a lagrange polynomial. 259 /// </summary> 260 /// <returns>A list of vectors representing the piecewise-linear approximation.</returns> 261 public static List<Vector2> ApproximateLagrangePolynomial(ReadOnlySpan<Vector2> controlPoints) 262 { 263 // TODO: add some smarter logic here, chebyshev nodes? 264 const int num_steps = 51; 265 266 var result = new List<Vector2>(num_steps); 267 268 double[] weights = Interpolation.BarycentricWeights(controlPoints); 269 270 float minX = controlPoints[0].X; 271 float maxX = controlPoints[0].X; 272 273 for (int i = 1; i < controlPoints.Length; i++) 274 { 275 minX = Math.Min(minX, controlPoints[i].X); 276 maxX = Math.Max(maxX, controlPoints[i].X); 277 } 278 279 float dx = maxX - minX; 280 281 for (int i = 0; i < num_steps; i++) 282 { 283 float x = minX + dx / (num_steps - 1) * i; 284 float y = (float)Interpolation.BarycentricLagrange(controlPoints, weights, x); 285 result.Add(new Vector2(x, y)); 286 } 287 288 return result; 289 } 290 291 private readonly struct CircularArcProperties 292 { 293 public readonly bool IsValid; 294 public readonly double ThetaStart; 295 public readonly double ThetaRange; 296 public readonly double Direction; 297 public readonly float Radius; 298 public readonly Vector2 Centre; 299 300 public double ThetaEnd => ThetaStart + ThetaRange * Direction; 301 302 public CircularArcProperties(double thetaStart, double thetaRange, double direction, float radius, Vector2 centre) 303 { 304 IsValid = true; 305 ThetaStart = thetaStart; 306 ThetaRange = thetaRange; 307 Direction = direction; 308 Radius = radius; 309 Centre = centre; 310 } 311 } 312 313 /// <summary> 314 /// Computes various properties that can be used to approximate the circular arc. 315 /// </summary> 316 /// <param name="controlPoints">Three distinct points on the arc.</param> 317 private static CircularArcProperties circularArcProperties(ReadOnlySpan<Vector2> controlPoints) 318 { 319 Vector2 a = controlPoints[0]; 320 Vector2 b = controlPoints[1]; 321 Vector2 c = controlPoints[2]; 322 323 // If we have a degenerate triangle where a side-length is almost zero, then give up and fallback to a more numerically stable method. 324 if (Precision.AlmostEquals(0, (b.Y - a.Y) * (c.X - a.X) - (b.X - a.X) * (c.Y - a.Y))) 325 return default; // Implicitly sets `IsValid` to false 326 327 // See: https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_2 328 float d = 2 * (a.X * (b - c).Y + b.X * (c - a).Y + c.X * (a - b).Y); 329 float aSq = a.LengthSquared; 330 float bSq = b.LengthSquared; 331 float cSq = c.LengthSquared; 332 333 Vector2 centre = new Vector2( 334 aSq * (b - c).Y + bSq * (c - a).Y + cSq * (a - b).Y, 335 aSq * (c - b).X + bSq * (a - c).X + cSq * (b - a).X) / d; 336 337 Vector2 dA = a - centre; 338 Vector2 dC = c - centre; 339 340 float r = dA.Length; 341 342 double thetaStart = Math.Atan2(dA.Y, dA.X); 343 double thetaEnd = Math.Atan2(dC.Y, dC.X); 344 345 while (thetaEnd < thetaStart) 346 thetaEnd += 2 * Math.PI; 347 348 double dir = 1; 349 double thetaRange = thetaEnd - thetaStart; 350 351 // Decide in which direction to draw the circle, depending on which side of 352 // AC B lies. 353 Vector2 orthoAtoC = c - a; 354 orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X); 355 356 if (Vector2.Dot(orthoAtoC, b - a) < 0) 357 { 358 dir = -dir; 359 thetaRange = 2 * Math.PI - thetaRange; 360 } 361 362 return new CircularArcProperties(thetaStart, thetaRange, dir, r, centre); 363 } 364 365 /// <summary> 366 /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds. 367 /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function 368 /// checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts 369 /// need to have a denser approximation to be more "flat". 370 /// </summary> 371 /// <param name="controlPoints">The control points to check for flatness.</param> 372 /// <returns>Whether the control points are flat enough.</returns> 373 private static bool bezierIsFlatEnough(Vector2[] controlPoints) 374 { 375 for (int i = 1; i < controlPoints.Length - 1; i++) 376 { 377 if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > bezier_tolerance * bezier_tolerance * 4) 378 return false; 379 } 380 381 return true; 382 } 383 384 /// <summary> 385 /// Subdivides n control points representing a bezier curve into 2 sets of n control points, each 386 /// describing a bezier curve equivalent to a half of the original curve. Effectively this splits 387 /// the original curve into 2 curves which result in the original curve when pieced back together. 388 /// </summary> 389 /// <param name="controlPoints">The control points to split.</param> 390 /// <param name="l">Output: The control points corresponding to the left half of the curve.</param> 391 /// <param name="r">Output: The control points corresponding to the right half of the curve.</param> 392 /// <param name="subdivisionBuffer">The first buffer containing the current subdivision state.</param> 393 /// <param name="count">The number of control points in the original list.</param> 394 private static void bezierSubdivide(Vector2[] controlPoints, Vector2[] l, Vector2[] r, Vector2[] subdivisionBuffer, int count) 395 { 396 Vector2[] midpoints = subdivisionBuffer; 397 398 for (int i = 0; i < count; ++i) 399 midpoints[i] = controlPoints[i]; 400 401 for (int i = 0; i < count; i++) 402 { 403 l[i] = midpoints[0]; 404 r[count - i - 1] = midpoints[count - i - 1]; 405 406 for (int j = 0; j < count - i - 1; j++) 407 midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2; 408 } 409 } 410 411 /// <summary> 412 /// This uses <a href="https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm">De Casteljau's algorithm</a> to obtain an optimal 413 /// piecewise-linear approximation of the bezier curve with the same amount of points as there are control points. 414 /// </summary> 415 /// <param name="controlPoints">The control points describing the bezier curve to be approximated.</param> 416 /// <param name="output">The points representing the resulting piecewise-linear approximation.</param> 417 /// <param name="count">The number of control points in the original list.</param> 418 /// <param name="subdivisionBuffer1">The first buffer containing the current subdivision state.</param> 419 /// <param name="subdivisionBuffer2">The second buffer containing the current subdivision state.</param> 420 private static void bezierApproximate(Vector2[] controlPoints, List<Vector2> output, Vector2[] subdivisionBuffer1, Vector2[] subdivisionBuffer2, int count) 421 { 422 Vector2[] l = subdivisionBuffer2; 423 Vector2[] r = subdivisionBuffer1; 424 425 bezierSubdivide(controlPoints, l, r, subdivisionBuffer1, count); 426 427 for (int i = 0; i < count - 1; ++i) 428 l[count + i] = r[i + 1]; 429 430 output.Add(controlPoints[0]); 431 432 for (int i = 1; i < count - 1; ++i) 433 { 434 int index = 2 * i; 435 Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]); 436 output.Add(p); 437 } 438 } 439 440 /// <summary> 441 /// Finds a point on the spline at the position of a parameter. 442 /// </summary> 443 /// <param name="vec1">The first vector.</param> 444 /// <param name="vec2">The second vector.</param> 445 /// <param name="vec3">The third vector.</param> 446 /// <param name="vec4">The fourth vector.</param> 447 /// <param name="t">The parameter at which to find the point on the spline, in the range [0, 1].</param> 448 /// <returns>The point on the spline at <paramref name="t"/>.</returns> 449 private static Vector2 catmullFindPoint(ref Vector2 vec1, ref Vector2 vec2, ref Vector2 vec3, ref Vector2 vec4, float t) 450 { 451 float t2 = t * t; 452 float t3 = t * t2; 453 454 Vector2 result; 455 result.X = 0.5f * (2f * vec2.X + (-vec1.X + vec3.X) * t + (2f * vec1.X - 5f * vec2.X + 4f * vec3.X - vec4.X) * t2 + (-vec1.X + 3f * vec2.X - 3f * vec3.X + vec4.X) * t3); 456 result.Y = 0.5f * (2f * vec2.Y + (-vec1.Y + vec3.Y) * t + (2f * vec1.Y - 5f * vec2.Y + 4f * vec3.Y - vec4.Y) * t2 + (-vec1.Y + 3f * vec2.Y - 3f * vec3.Y + vec4.Y) * t3); 457 458 return result; 459 } 460 } 461}