diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs index 0e6d991830..bd2e018a95 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens.Testing; using osu.Framework.Timing; +using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.Osu.Objects; using osu.Game.Modes.Osu.Objects.Drawables; @@ -60,12 +61,15 @@ namespace osu.Desktop.VisualTests.Tests add(new DrawableSlider(new Slider { StartTime = framedClock.CurrentTime + 600, - ControlPoints = new List + CurveObject = new CurvedHitObject { - new Vector2(-200, 0), - new Vector2(400, 0), + ControlPoints = new List + { + new Vector2(-200, 0), + new Vector2(400, 0), + }, + Distance = 400 }, - Distance = 400, Position = new Vector2(-200, 0), Velocity = 1, TickDistance = 100, diff --git a/osu.Game.Modes.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Modes.Osu/Beatmaps/OsuBeatmapConverter.cs index 36966583df..ea5143b08a 100644 --- a/osu.Game.Modes.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Modes.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -34,10 +34,8 @@ namespace osu.Game.Modes.Osu.Beatmaps private OsuHitObject convertHitObject(HitObject original) { IHasCurve curveData = original as IHasCurve; - IHasDistance distanceData = original as IHasDistance; IHasEndTime endTimeData = original as IHasEndTime; IHasPosition positionData = original as IHasPosition; - IHasRepeats repeatsData = original as IHasRepeats; IHasCombo comboData = original as IHasCombo; if (curveData != null) @@ -47,16 +45,11 @@ namespace osu.Game.Modes.Osu.Beatmaps StartTime = original.StartTime, Sample = original.Sample, - CurveType = curveData.CurveType, - ControlPoints = curveData.ControlPoints, + CurveObject = curveData, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false, - - Distance = distanceData?.Distance ?? 0, - - RepeatCount = repeatsData?.RepeatCount ?? 0 + NewCombo = comboData?.NewCombo ?? false }; } @@ -68,7 +61,7 @@ namespace osu.Game.Modes.Osu.Beatmaps Sample = original.Sample, Position = new Vector2(512, 384) / 2, - EndTime = endTimeData.EndTime, + EndTime = endTimeData.EndTime }; } diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs index aa38609c7c..2eb32821a3 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs @@ -72,7 +72,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables AddNested(initialCircle); - var repeatDuration = s.Curve.Length / s.Velocity; + var repeatDuration = s.Curve.Distance / s.Velocity; foreach (var tick in s.Ticks) { var repeatStartTime = s.StartTime + tick.RepeatIndex * repeatDuration; @@ -104,7 +104,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables double progress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); int repeat = slider.RepeatAt(progress); - progress = slider.CurveProgressAt(progress); + progress = slider.ProgressAt(progress); if (repeat > currentRepeat) { diff --git a/osu.Game.Modes.Osu/Objects/Slider.cs b/osu.Game.Modes.Osu/Objects/Slider.cs index 7aecdb1240..83a5a458c7 100644 --- a/osu.Game.Modes.Osu/Objects/Slider.cs +++ b/osu.Game.Modes.Osu/Objects/Slider.cs @@ -8,40 +8,30 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Modes.Objects.Types; using System; using System.Collections.Generic; +using osu.Game.Modes.Objects; namespace osu.Game.Modes.Osu.Objects { - public class Slider : OsuHitObject, IHasEndTime, IHasCurve, IHasDistance, IHasRepeats + public class Slider : OsuHitObject, IHasCurve { - public double EndTime => StartTime + RepeatCount * Curve.Length / Velocity; + public IHasCurve CurveObject { get; set; } + + public SliderCurve Curve => CurveObject.Curve; + + public double EndTime => StartTime + RepeatCount * Curve.Distance / Velocity; public double Duration => EndTime - StartTime; public override Vector2 EndPosition => PositionAt(1); - /// - /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the slider) - /// to 1 (end of the slider). This includes repeat logic. - /// - /// Ranges from 0 (beginning of the slider) to 1 (end of the slider). - /// - public Vector2 PositionAt(double progress) => Curve.PositionAt(CurveProgressAt(progress)); + public Vector2 PositionAt(double progress) => CurveObject.PositionAt(progress); + public double ProgressAt(double progress) => CurveObject.ProgressAt(progress); + public int RepeatAt(double progress) => CurveObject.RepeatAt(progress); - /// - /// Find the current progress along the curve, accounting for repeat logic. - /// - public double CurveProgressAt(double progress) - { - var p = progress * RepeatCount % 1; - if (RepeatAt(progress) % 2 == 1) - p = 1 - p; - return p; - } + public List ControlPoints => CurveObject.ControlPoints; + public CurveType CurveType => CurveObject.CurveType; + public double Distance => CurveObject.Distance; - /// - /// Determine which repeat of the slider we are on at a given progress. - /// Range is 0..RepeatCount where 0 is the first run. - /// - public int RepeatAt(double progress) => (int)(progress * RepeatCount); + public int RepeatCount => CurveObject.RepeatCount; private int stackHeight; public override int StackHeight @@ -54,24 +44,6 @@ namespace osu.Game.Modes.Osu.Objects } } - public List ControlPoints - { - get { return Curve.ControlPoints; } - set { Curve.ControlPoints = value; } - } - - public double Distance - { - get { return Curve.Length; } - set { Curve.Length = value; } - } - - public CurveType CurveType - { - get { return Curve.CurveType; } - set { Curve.CurveType = value; } - } - public double Velocity; public double TickDistance; @@ -90,17 +62,13 @@ namespace osu.Game.Modes.Osu.Objects TickDistance = baseVelocity / baseDifficulty.SliderTickRate; } - public int RepeatCount { get; set; } = 1; - - internal readonly SliderCurve Curve = new SliderCurve(); - public IEnumerable Ticks { get { if (TickDistance == 0) yield break; - var length = Curve.Length; + var length = Curve.Distance; var tickDistance = Math.Min(TickDistance, length); var repeatDuration = length / Velocity; diff --git a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj index 38d392dc25..9b66dc0bc1 100644 --- a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj +++ b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj @@ -70,7 +70,6 @@ - diff --git a/osu.Game/Modes/Objects/BezierApproximator.cs b/osu.Game/Modes/Objects/BezierApproximator.cs new file mode 100644 index 0000000000..b0f17afc7e --- /dev/null +++ b/osu.Game/Modes/Objects/BezierApproximator.cs @@ -0,0 +1,150 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using OpenTK; + +namespace osu.Game.Modes.Objects +{ + public class BezierApproximator + { + private int count; + private List controlPoints; + private Vector2[] subdivisionBuffer1; + private Vector2[] subdivisionBuffer2; + + private const float tolerance = 0.25f; + private const float tolerance_sq = tolerance * tolerance; + + public BezierApproximator(List controlPoints) + { + this.controlPoints = controlPoints; + count = controlPoints.Count; + + subdivisionBuffer1 = new Vector2[count]; + subdivisionBuffer2 = new Vector2[count * 2 - 1]; + } + + /// + /// 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); + } + } + + /// + /// 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 CreateBezier() + { + List output = new List(); + + if (count == 0) + return output; + + 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; + } + } +} \ No newline at end of file diff --git a/osu.Game/Modes/Objects/CircularArcApproximator.cs b/osu.Game/Modes/Objects/CircularArcApproximator.cs new file mode 100644 index 0000000000..af15f99e43 --- /dev/null +++ b/osu.Game/Modes/Objects/CircularArcApproximator.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2007-2017 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.Modes.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 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. + 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/Modes/Objects/CurvedHitObject.cs b/osu.Game/Modes/Objects/CurvedHitObject.cs new file mode 100644 index 0000000000..319547e91d --- /dev/null +++ b/osu.Game/Modes/Objects/CurvedHitObject.cs @@ -0,0 +1,46 @@ +using osu.Game.Modes.Objects.Types; +using System.Collections.Generic; +using OpenTK; + +namespace osu.Game.Modes.Objects +{ + public class CurvedHitObject : HitObject, IHasCurve + { + public SliderCurve Curve { get; } = new SliderCurve(); + + public int RepeatCount { get; set; } = 1; + + public double EndTime => 0; + public double Duration => 0; + + public List ControlPoints + { + get { return Curve.ControlPoints; } + set { Curve.ControlPoints = value; } + } + + public CurveType CurveType + { + get { return Curve.CurveType; } + set { Curve.CurveType = value; } + } + + public double Distance + { + get { return Curve.Distance; } + set { Curve.Distance = value; } + } + + public Vector2 PositionAt(double progress) => Curve.PositionAt(ProgressAt(progress)); + + public double ProgressAt(double progress) + { + var p = progress * RepeatCount % 1; + if (RepeatAt(progress) % 2 == 1) + p = 1 - p; + return p; + } + + public int RepeatAt(double progress) => (int)(progress * RepeatCount); + } +} diff --git a/osu.Game/Modes/Objects/Legacy/LegacySlider.cs b/osu.Game/Modes/Objects/Legacy/LegacySlider.cs index fb5b41aad9..bdfebb1983 100644 --- a/osu.Game/Modes/Objects/Legacy/LegacySlider.cs +++ b/osu.Game/Modes/Objects/Legacy/LegacySlider.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Game.Modes.Objects.Types; using OpenTK; @@ -10,17 +9,10 @@ namespace osu.Game.Modes.Objects.Legacy /// /// Legacy Slider-type, used for parsing Beatmaps. /// - public sealed class LegacySlider : HitObject, IHasCurve, IHasPosition, IHasDistance, IHasRepeats, IHasCombo + public sealed class LegacySlider : CurvedHitObject, IHasPosition, IHasCombo { - public List ControlPoints { get; set; } - public CurveType CurveType { get; set; } - public Vector2 Position { get; set; } - public double Distance { get; set; } - - public int RepeatCount { get; set; } - public bool NewCombo { get; set; } } } diff --git a/osu.Game.Modes.Osu/Objects/SliderCurve.cs b/osu.Game/Modes/Objects/SliderCurve.cs similarity index 94% rename from osu.Game.Modes.Osu/Objects/SliderCurve.cs rename to osu.Game/Modes/Objects/SliderCurve.cs index b32ace5c0d..8ab5097257 100644 --- a/osu.Game.Modes.Osu/Objects/SliderCurve.cs +++ b/osu.Game/Modes/Objects/SliderCurve.cs @@ -2,16 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using OpenTK; using System.Linq; using osu.Framework.MathUtils; using osu.Game.Modes.Objects.Types; +using OpenTK; -namespace osu.Game.Modes.Osu.Objects +namespace osu.Game.Modes.Objects { public class SliderCurve { - public double Length; + public double Distance; public List ControlPoints; @@ -83,12 +83,12 @@ namespace osu.Game.Modes.Osu.Objects // Shorten slider curves that are too long compared to what's // in the .osu file. - if (Length - l < d) + if (Distance - l < d) { - calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Length - l) / d); + calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Distance - l) / d); calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i); - l = Length; + l = Distance; cumulativeLength.Add(l); break; } @@ -130,7 +130,7 @@ namespace osu.Game.Modes.Osu.Objects private double progressToDistance(double progress) { - return MathHelper.Clamp(progress, 0, 1) * Length; + return MathHelper.Clamp(progress, 0, 1) * Distance; } private Vector2 interpolateVertices(int i, double d) diff --git a/osu.Game/Modes/Objects/Types/IHasCurve.cs b/osu.Game/Modes/Objects/Types/IHasCurve.cs index 2eb40dc96a..0db799a15f 100644 --- a/osu.Game/Modes/Objects/Types/IHasCurve.cs +++ b/osu.Game/Modes/Objects/Types/IHasCurve.cs @@ -9,8 +9,13 @@ namespace osu.Game.Modes.Objects.Types /// /// A HitObject that has a curve. /// - public interface IHasCurve : IHasDistance + public interface IHasCurve : IHasDistance, IHasRepeats { + /// + /// The curve. + /// + SliderCurve Curve { get; } + /// /// The control points that shape the curve. /// @@ -20,5 +25,28 @@ namespace osu.Game.Modes.Objects.Types /// The type of curve. /// CurveType CurveType { get; } + + /// + /// Computes the position on the curve at a given progress, accounting for repeat logic. + /// + /// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// + /// + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + Vector2 PositionAt(double progress); + + /// + /// Finds the progress along the curve, accounting for repeat logic. + /// + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + double ProgressAt(double progress); + + /// + /// Determines which repeat of the curve the progress point is on. + /// + /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, RepeatCount] where 0 is the first run. + int RepeatAt(double progress); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0cf3167101..678e89f0a8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -94,11 +94,15 @@ + + + +