1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 07:22:55 +08:00

Add circular arc approximator for "perfect" sliders.

This commit is contained in:
Thomas Müller 2016-12-09 18:04:02 +01:00
parent 359cb5ac6a
commit 38968ad6d2
4 changed files with 137 additions and 19 deletions

View File

@ -0,0 +1,102 @@
//Copyright (c) 2007-2016 ppy Pty Ltd <contact@ppy.sh>.
//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;
}
/// <summary>
/// Creates a piecewise-linear approximation of a circular arc curve.
/// </summary>
/// <returns>A list of vectors representing the piecewise-linear approximation.</returns>
public List<Vector2> 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<Vector2>();
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>();
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<Vector2> output = new List<Vector2>(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;
}
}
}

View File

@ -83,7 +83,7 @@ namespace osu.Game.Modes.Osu.Objects
s.Curve = new SliderCurve
{
Path = points,
ControlPoints = points,
Length = length,
CurveType = curveType
};

View File

@ -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<Vector2> Path;
public List<Vector2> ControlPoints;
public CurveTypes CurveType;
private List<Vector2> calculatedPath = new List<Vector2>();
private List<double> cumulativeLength = new List<double>();
private List<Vector2> calculateSubpath(List<Vector2> subpath)
private List<Vector2> calculateSubpath(List<Vector2> subControlPoints)
{
switch (CurveType)
{
case CurveTypes.Linear:
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<Vector2> 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<Vector2> subpath = new List<Vector2>();
for (int i = 0; i < Path.Count; ++i)
List<Vector2> subControlPoints = new List<Vector2>();
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<Vector2> 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();
}
}
}

View File

@ -42,6 +42,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Objects\BezierApproximator.cs" />
<Compile Include="Objects\CircularArcApproximator.cs" />
<Compile Include="Objects\Drawables\DrawableOsuHitObject.cs" />
<Compile Include="Objects\Drawables\Pieces\ApproachCircle.cs" />
<Compile Include="Objects\Drawables\Pieces\CirclePiece.cs" />