Merge pull request #27734 from smoogipoo/fix-catmull-bulbs

Fix bulbs on Catmull sliders
This commit is contained in:
Bartłomiej Dach 2024-04-10 11:49:29 +02:00 committed by GitHub
commit b559bec49f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 84 additions and 3 deletions

View File

@ -41,7 +41,7 @@ public double Duration
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
private readonly SliderPath path = new SliderPath();
private readonly SliderPath path = new SliderPath { OptimiseCatmull = true };
public SliderPath Path
{

View File

@ -42,6 +42,17 @@ public class SliderPath
private readonly List<double> cumulativeLength = new List<double>();
private readonly Cached pathCache = new Cached();
/// <summary>
/// Any additional length of the path which was optimised out during piecewise approximation, but should still be considered as part of <see cref="calculatedLength"/>.
/// </summary>
/// <remarks>
/// This is a hack for Catmull paths.
/// </remarks>
private double optimisedLength;
/// <summary>
/// The final calculated length of the path.
/// </summary>
private double calculatedLength;
private readonly List<int> segmentEnds = new List<int>();
@ -123,6 +134,24 @@ public double CalculatedDistance
}
}
private bool optimiseCatmull;
/// <summary>
/// Whether to optimise Catmull path segments, usually resulting in removing bulbs around stacked knots.
/// </summary>
/// <remarks>
/// This changes the path shape and should therefore not be used.
/// </remarks>
public bool OptimiseCatmull
{
get => optimiseCatmull;
set
{
optimiseCatmull = value;
invalidate();
}
}
/// <summary>
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
/// to 1 (end of the slider) and stores the generated path in the given list.
@ -244,6 +273,7 @@ private void calculatePath()
{
calculatedPath.Clear();
segmentEnds.Clear();
optimisedLength = 0;
if (ControlPoints.Count == 0)
return;
@ -269,6 +299,7 @@ private void calculatePath()
else if (segmentVertices.Length > 1)
{
List<Vector2> subPath = calculateSubPath(segmentVertices, segmentType);
// Skip the first vertex if it is the same as the last vertex from the previous segment
bool skipFirst = calculatedPath.Count > 0 && subPath.Count > 0 && calculatedPath.Last() == subPath[0];
@ -295,6 +326,7 @@ private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, P
return PathApproximator.LinearToPiecewiseLinear(subControlPoints);
case SplineType.PerfectCurve:
{
if (subControlPoints.Length != 3)
break;
@ -305,9 +337,58 @@ private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, P
break;
return subPath;
}
case SplineType.Catmull:
return PathApproximator.CatmullToPiecewiseLinear(subControlPoints);
{
List<Vector2> subPath = PathApproximator.CatmullToPiecewiseLinear(subControlPoints);
if (!OptimiseCatmull)
return subPath;
// At draw time, osu!stable optimises paths by only keeping piecewise segments that are 6px apart.
// For the most part we don't care about this optimisation, and its additional heuristics are hard to reproduce in every implementation.
//
// However, it matters for Catmull paths which form "bulbs" around sequential knots with identical positions,
// so we'll apply a very basic form of the optimisation here and return a length representing the optimised portion.
// The returned length is important so that the optimisation doesn't cause the path to get extended to match the value of ExpectedDistance.
List<Vector2> optimisedPath = new List<Vector2>(subPath.Count);
Vector2? lastStart = null;
double lengthRemovedSinceStart = 0;
for (int i = 0; i < subPath.Count; i++)
{
if (lastStart == null)
{
optimisedPath.Add(subPath[i]);
lastStart = subPath[i];
continue;
}
Debug.Assert(i > 0);
double distFromStart = Vector2.Distance(lastStart.Value, subPath[i]);
lengthRemovedSinceStart += Vector2.Distance(subPath[i - 1], subPath[i]);
// See PathApproximator.catmull_detail.
const int catmull_detail = 50;
const int catmull_segment_length = catmull_detail * 2;
// Either 6px from the start, the last vertex at every knot, or the end of the path.
if (distFromStart > 6 || (i + 1) % catmull_segment_length == 0 || i == subPath.Count - 1)
{
optimisedPath.Add(subPath[i]);
optimisedLength += lengthRemovedSinceStart - distFromStart;
lastStart = null;
lengthRemovedSinceStart = 0;
}
}
return optimisedPath;
}
}
return PathApproximator.BSplineToPiecewiseLinear(subControlPoints, type.Degree ?? subControlPoints.Length);
@ -315,7 +396,7 @@ private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, P
private void calculateLength()
{
calculatedLength = 0;
calculatedLength = optimisedLength;
cumulativeLength.Clear();
cumulativeLength.Add(0);