From e6ee3dc73ee7b784929a5635c8412ea161906be1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Nov 2018 02:43:43 +0900 Subject: [PATCH] Use framework helper functions for path approximation --- .../Masks/SliderMasks/SliderPlacementMask.cs | 6 +- .../Rulesets/Objects/BezierApproximator.cs | 145 ------------------ .../Rulesets/Objects/CatmullApproximator.cs | 63 -------- .../Objects/CircularArcApproximator.cs | 90 ----------- osu.Game/Rulesets/Objects/IApproximator.cs | 19 --- .../Rulesets/Objects/LinearApproximator.cs | 22 --- osu.Game/Rulesets/Objects/SliderPath.cs | 8 +- osu.Game/osu.Game.csproj | 2 +- 8 files changed, 8 insertions(+), 347 deletions(-) delete mode 100644 osu.Game/Rulesets/Objects/BezierApproximator.cs delete mode 100644 osu.Game/Rulesets/Objects/CatmullApproximator.cs delete mode 100644 osu.Game/Rulesets/Objects/CircularArcApproximator.cs delete mode 100644 osu.Game/Rulesets/Objects/IApproximator.cs delete mode 100644 osu.Game/Rulesets/Objects/LinearApproximator.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderPlacementMask.cs b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderPlacementMask.cs index 8b38c25bd4..12e768d58e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderPlacementMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Masks/SliderMasks/SliderPlacementMask.cs @@ -8,9 +8,9 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks.Components; using OpenTK; @@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Masks.SliderMasks { case 1: case 2: - result = new LinearApproximator().Approximate(allControlPoints); + result = PathApproximator.ApproximateLinear(allControlPoints); break; default: - result = new BezierApproximator().Approximate(allControlPoints); + result = PathApproximator.ApproximateBezier(allControlPoints); break; } diff --git a/osu.Game/Rulesets/Objects/BezierApproximator.cs b/osu.Game/Rulesets/Objects/BezierApproximator.cs deleted file mode 100644 index 68833b655a..0000000000 --- a/osu.Game/Rulesets/Objects/BezierApproximator.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; - -namespace osu.Game.Rulesets.Objects -{ - public struct BezierApproximator : IApproximator - { - private const float tolerance = 0.25f; - private const float tolerance_sq = tolerance * tolerance; - - private int count; - private Vector2[] subdivisionBuffer1; - private Vector2[] subdivisionBuffer2; - - /// - /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing - /// the control points until their approximation error vanishes below a given threshold. - /// - /// A list of vectors representing the piecewise-linear approximation. - public List Approximate(ReadOnlySpan controlPoints) - { - List output = new List(); - count = controlPoints.Length; - - if (count == 0) - return output; - - subdivisionBuffer1 = new Vector2[count]; - subdivisionBuffer2 = new Vector2[count * 2 - 1]; - - Stack toFlatten = new Stack(); - Stack freeBuffers = new Stack(); - - // "toFlatten" contains all the curves which are not yet approximated well enough. - // We use a stack to emulate recursion without the risk of running into a stack overflow. - // (More specifically, we iteratively and adaptively refine our curve with a - // Depth-first search - // over the tree resulting from the subdivisions we make.) - toFlatten.Push(controlPoints.ToArray()); - - Vector2[] leftChild = subdivisionBuffer2; - - while (toFlatten.Count > 0) - { - Vector2[] parent = toFlatten.Pop(); - if (isFlatEnough(parent)) - { - // If the control points we currently operate on are sufficiently "flat", we use - // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation - // of the bezier curve represented by our control points, consisting of the same amount - // of points as there are control points. - approximate(parent, output); - freeBuffers.Push(parent); - continue; - } - - // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep - // subdividing the curve we are currently operating on. - Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[count]; - subdivide(parent, leftChild, rightChild); - - // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration. - for (int i = 0; i < count; ++i) - parent[i] = leftChild[i]; - - toFlatten.Push(rightChild); - toFlatten.Push(parent); - } - - output.Add(controlPoints[count - 1]); - return output; - } - - /// - /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds. - /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function - /// checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts - /// need to have a denser approximation to be more "flat". - /// - /// The control points to check for flatness. - /// Whether the control points are flat enough. - private static bool isFlatEnough(Vector2[] controlPoints) - { - for (int i = 1; i < controlPoints.Length - 1; i++) - if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > tolerance_sq * 4) - return false; - - return true; - } - - /// - /// Subdivides n control points representing a bezier curve into 2 sets of n control points, each - /// describing a bezier curve equivalent to a half of the original curve. Effectively this splits - /// the original curve into 2 curves which result in the original curve when pieced back together. - /// - /// The control points to split. - /// Output: The control points corresponding to the left half of the curve. - /// Output: The control points corresponding to the right half of the curve. - private void subdivide(Vector2[] controlPoints, Vector2[] l, Vector2[] r) - { - Vector2[] midpoints = subdivisionBuffer1; - - for (int i = 0; i < count; ++i) - midpoints[i] = controlPoints[i]; - - for (int i = 0; i < count; i++) - { - l[i] = midpoints[0]; - r[count - i - 1] = midpoints[count - i - 1]; - - for (int j = 0; j < count - i - 1; j++) - midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2; - } - } - - /// - /// This uses De Casteljau's algorithm to obtain an optimal - /// piecewise-linear approximation of the bezier curve with the same amount of points as there are control points. - /// - /// The control points describing the bezier curve to be approximated. - /// The points representing the resulting piecewise-linear approximation. - private void approximate(Vector2[] controlPoints, List output) - { - Vector2[] l = subdivisionBuffer2; - Vector2[] r = subdivisionBuffer1; - - subdivide(controlPoints, l, r); - - for (int i = 0; i < count - 1; ++i) - l[count + i] = r[i + 1]; - - output.Add(controlPoints[0]); - for (int i = 1; i < count - 1; ++i) - { - int index = 2 * i; - Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]); - output.Add(p); - } - } - } -} diff --git a/osu.Game/Rulesets/Objects/CatmullApproximator.cs b/osu.Game/Rulesets/Objects/CatmullApproximator.cs deleted file mode 100644 index 5712b508c4..0000000000 --- a/osu.Game/Rulesets/Objects/CatmullApproximator.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; - -namespace osu.Game.Rulesets.Objects -{ - public readonly struct CatmullApproximator : IApproximator - { - /// - /// The amount of pieces to calculate for each controlpoint quadruplet. - /// - private const int detail = 50; - - /// - /// Creates a piecewise-linear approximation of a Catmull-Rom spline. - /// - /// A list of vectors representing the piecewise-linear approximation. - public List Approximate(ReadOnlySpan controlPoints) - { - var result = new List((controlPoints.Length - 1) * detail * 2); - - for (int i = 0; i < controlPoints.Length - 1; i++) - { - var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i]; - var v2 = controlPoints[i]; - var v3 = i < controlPoints.Length - 1 ? controlPoints[i + 1] : v2 + v2 - v1; - var v4 = i < controlPoints.Length - 2 ? controlPoints[i + 2] : v3 + v3 - v2; - - for (int c = 0; c < detail; c++) - { - result.Add(findPoint(ref v1, ref v2, ref v3, ref v4, (float)c / detail)); - result.Add(findPoint(ref v1, ref v2, ref v3, ref v4, (float)(c + 1) / detail)); - } - } - - return result; - } - - /// - /// Finds a point on the spline at the position of a parameter. - /// - /// The first vector. - /// The second vector. - /// The third vector. - /// The fourth vector. - /// The parameter at which to find the point on the spline, in the range [0, 1]. - /// The point on the spline at . - private Vector2 findPoint(ref Vector2 vec1, ref Vector2 vec2, ref Vector2 vec3, ref Vector2 vec4, float t) - { - float t2 = t * t; - float t3 = t * t2; - - Vector2 result; - 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); - 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); - - return result; - } - } -} diff --git a/osu.Game/Rulesets/Objects/CircularArcApproximator.cs b/osu.Game/Rulesets/Objects/CircularArcApproximator.cs deleted file mode 100644 index 969a98c48f..0000000000 --- a/osu.Game/Rulesets/Objects/CircularArcApproximator.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.MathUtils; -using OpenTK; - -namespace osu.Game.Rulesets.Objects -{ - public readonly struct CircularArcApproximator : IApproximator - { - private const float tolerance = 0.1f; - - /// - /// Creates a piecewise-linear approximation of a circular arc curve. - /// - /// A list of vectors representing the piecewise-linear approximation. - public List Approximate(ReadOnlySpan controlPoints) - { - Vector2 a = controlPoints[0]; - Vector2 b = controlPoints[1]; - Vector2 c = controlPoints[2]; - - float aSq = (b - c).LengthSquared; - float bSq = (a - c).LengthSquared; - float cSq = (a - b).LengthSquared; - - // If we have a degenerate triangle where a side-length is almost zero, then give up and fall - // back to a more numerically stable method. - if (Precision.AlmostEquals(aSq, 0) || Precision.AlmostEquals(bSq, 0) || Precision.AlmostEquals(cSq, 0)) - return new List(); - - float s = aSq * (bSq + cSq - aSq); - float t = bSq * (aSq + cSq - bSq); - float u = cSq * (aSq + bSq - cSq); - - float sum = s + t + u; - - // If we have a degenerate triangle with an almost-zero size, then give up and fall - // back to a more numerically stable method. - if (Precision.AlmostEquals(sum, 0)) - return new List(); - - Vector2 centre = (s * a + t * b + u * c) / sum; - Vector2 dA = a - centre; - Vector2 dC = c - centre; - - float r = dA.Length; - - double thetaStart = Math.Atan2(dA.Y, dA.X); - double thetaEnd = Math.Atan2(dC.Y, dC.X); - - while (thetaEnd < thetaStart) - thetaEnd += 2 * Math.PI; - - double dir = 1; - double thetaRange = thetaEnd - thetaStart; - - // Decide in which direction to draw the circle, depending on which side of - // AC B lies. - Vector2 orthoAtoC = c - a; - orthoAtoC = new Vector2(orthoAtoC.Y, -orthoAtoC.X); - if (Vector2.Dot(orthoAtoC, b - a) < 0) - { - dir = -dir; - thetaRange = 2 * Math.PI - thetaRange; - } - - // We select the amount of points for the approximation by requiring the discrete curvature - // to be smaller than the provided tolerance. The exact angle required to meet the tolerance - // is: 2 * Math.Acos(1 - TOLERANCE / r) - // The special case is required for extremely short sliders where the radius is smaller than - // the tolerance. This is a pathological rather than a realistic case. - int amountPoints = 2 * r <= tolerance ? 2 : Math.Max(2, (int)Math.Ceiling(thetaRange / (2 * Math.Acos(1 - tolerance / r)))); - - List output = new List(amountPoints); - - for (int i = 0; i < amountPoints; ++i) - { - double fract = (double)i / (amountPoints - 1); - double theta = thetaStart + dir * fract * thetaRange; - Vector2 o = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * r; - output.Add(centre + o); - } - - return output; - } - } -} diff --git a/osu.Game/Rulesets/Objects/IApproximator.cs b/osu.Game/Rulesets/Objects/IApproximator.cs deleted file mode 100644 index 4f242993bc..0000000000 --- a/osu.Game/Rulesets/Objects/IApproximator.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; - -namespace osu.Game.Rulesets.Objects -{ - public interface IApproximator - { - /// - /// Approximates a path by interpolating a sequence of control points. - /// - /// The control points of the path. - /// A set of points that lie on the path. - List Approximate(ReadOnlySpan controlPoints); - } -} diff --git a/osu.Game/Rulesets/Objects/LinearApproximator.cs b/osu.Game/Rulesets/Objects/LinearApproximator.cs deleted file mode 100644 index 1f36881fda..0000000000 --- a/osu.Game/Rulesets/Objects/LinearApproximator.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using OpenTK; - -namespace osu.Game.Rulesets.Objects -{ - public readonly struct LinearApproximator : IApproximator - { - public List Approximate(ReadOnlySpan controlPoints) - { - var result = new List(controlPoints.Length); - - foreach (var c in controlPoints) - result.Add(c); - - return result; - } - } -} diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index a141051308..423cd3b069 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -28,14 +28,14 @@ namespace osu.Game.Rulesets.Objects switch (PathType) { case PathType.Linear: - return new LinearApproximator().Approximate(subControlPoints); + return PathApproximator.ApproximateLinear(subControlPoints); case PathType.PerfectCurve: //we can only use CircularArc iff we have exactly three control points and no dissection. if (ControlPoints.Length != 3 || subControlPoints.Length != 3) break; // Here we have exactly 3 control points. Attempt to fit a circular arc. - List subpath = new CircularArcApproximator().Approximate(subControlPoints); + List subpath = PathApproximator.ApproximateCircularArc(subControlPoints); // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation. if (subpath.Count == 0) @@ -43,10 +43,10 @@ namespace osu.Game.Rulesets.Objects return subpath; case PathType.Catmull: - return new CatmullApproximator().Approximate(subControlPoints); + return PathApproximator.ApproximateCatmull(subControlPoints); } - return new BezierApproximator().Approximate(subControlPoints); + return PathApproximator.ApproximateBezier(subControlPoints); } private void calculatePath() diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2f8c743bee..b4cd8f9b66 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +