A game framework written with osu! in mind.
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}