diff --git a/osu.Game.Modes.Osu/Objects/CircularArcApproximator.cs b/osu.Game.Modes.Osu/Objects/CircularArcApproximator.cs new file mode 100644 index 0000000000..b8f84ed510 --- /dev/null +++ b/osu.Game.Modes.Osu/Objects/CircularArcApproximator.cs @@ -0,0 +1,102 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.MathUtils; +using System; +using System.Collections.Generic; + +namespace osu.Game.Modes.Osu.Objects +{ + public class CircularArcApproximator + { + private Vector2 A; + private Vector2 B; + private Vector2 C; + + private int amountPoints; + + private const float TOLERANCE = 0.1f; + + public CircularArcApproximator(Vector2 A, Vector2 B, Vector2 C) + { + this.A = A; + this.B = B; + this.C = C; + } + + /// + /// Creates a piecewise-linear approximation of a circular arc curve. + /// + /// A list of vectors representing the piecewise-linear approximation. + public List CreateArc() + { + 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 orthoAC = C - A; + orthoAC = new Vector2(orthoAC.Y, -orthoAC.X); + if (Vector2.Dot(orthoAC, 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) + if (2 * r <= TOLERANCE) + // This 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. + amountPoints = 2; + else + amountPoints = 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.Modes.Osu/Objects/OsuHitObjectParser.cs b/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs index 216c40b779..9516006c57 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObjectParser.cs @@ -83,7 +83,7 @@ namespace osu.Game.Modes.Osu.Objects s.Curve = new SliderCurve { - Path = points, + ControlPoints = points, Length = length, CurveType = curveType }; diff --git a/osu.Game.Modes.Osu/Objects/SliderCurve.cs b/osu.Game.Modes.Osu/Objects/SliderCurve.cs index e8df4049f5..961658112f 100644 --- a/osu.Game.Modes.Osu/Objects/SliderCurve.cs +++ b/osu.Game.Modes.Osu/Objects/SliderCurve.cs @@ -4,9 +4,8 @@ using System.Collections.Generic; using OpenTK; using System.Linq; -using System.Diagnostics; using osu.Framework.MathUtils; -using System; +using System.Diagnostics; namespace osu.Game.Modes.Osu.Objects { @@ -14,21 +13,39 @@ namespace osu.Game.Modes.Osu.Objects { public double Length; - public List Path; + public List ControlPoints; public CurveTypes CurveType; private List calculatedPath = new List(); private List cumulativeLength = new List(); - private List calculateSubpath(List subpath) + private List calculateSubpath(List subControlPoints) { switch (CurveType) { case CurveTypes.Linear: - return subpath; + return subControlPoints; + case CurveTypes.PerfectCurve: + // If we have a different amount than 3 control points, use bezier for perfect curves. + if (ControlPoints.Count != 3) + return new BezierApproximator(subControlPoints).CreateBezier(); + else + { + Debug.Assert(subControlPoints.Count == 3); + + // Here we have exactly 3 control points. Attempt to fit a circular arc. + List subpath = new CircularArcApproximator(subControlPoints[0], subControlPoints[1], subControlPoints[2]).CreateArc(); + + if (subpath.Count == 0) + // For some reason a circular arc could not be fit to the 3 given points. Fall back + // to a numerically stable bezier approximation. + subpath = new BezierApproximator(subControlPoints).CreateBezier(); + + return subpath; + } default: - return new BezierApproximator(subpath).CreateBezier(); + return new BezierApproximator(subControlPoints).CreateBezier(); } } @@ -39,21 +56,19 @@ namespace osu.Game.Modes.Osu.Objects // Sliders may consist of various subpaths separated by two consecutive vertices // with the same position. The following loop parses these subpaths and computes // their shape independently, consecutively appending them to calculatedPath. - List subpath = new List(); - for (int i = 0; i < Path.Count; ++i) + List subControlPoints = new List(); + for (int i = 0; i < ControlPoints.Count; ++i) { - subpath.Add(Path[i]); - if (i == Path.Count - 1 || Path[i] == Path[i + 1]) + subControlPoints.Add(ControlPoints[i]); + if (i == ControlPoints.Count - 1 || ControlPoints[i] == ControlPoints[i + 1]) { - // If we already constructed a subpath previously, then the new subpath - // will have as starting position the end position of the previous subpath. - // Hence we can and should remove the previous endpoint to avoid a segment - // with 0 length. - if (calculatedPath.Count > 0) - calculatedPath.RemoveAt(calculatedPath.Count - 1); + List subpath = calculateSubpath(subControlPoints); + for (int j = 0; j < subpath.Count; ++j) + // Only add those vertices that add a new segment to the path. + if (calculatedPath.Count == 0 || calculatedPath.Last() != subpath[j]) + calculatedPath.Add(subpath[j]); - calculatedPath.AddRange(calculateSubpath(subpath)); - subpath.Clear(); + subControlPoints.Clear(); } } } diff --git a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj index 503fabd28d..a9a346f563 100644 --- a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj +++ b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj @@ -42,6 +42,7 @@ +