Skip to content

Commit

Permalink
Merge pull request #27734 from smoogipoo/fix-catmull-bulbs
Browse files Browse the repository at this point in the history
Fix bulbs on Catmull sliders
  • Loading branch information
bdach authored Apr 10, 2024
2 parents 6cb5bff + 518addf commit b559bec
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 3 deletions.
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Osu/Objects/Slider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
85 changes: 83 additions & 2 deletions osu.Game/Rulesets/Objects/SliderPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -244,6 +273,7 @@ private void calculatePath()
{
calculatedPath.Clear();
segmentEnds.Clear();
optimisedLength = 0;

if (ControlPoints.Count == 0)
return;
Expand All @@ -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];

Expand All @@ -295,6 +326,7 @@ private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, P
return PathApproximator.LinearToPiecewiseLinear(subControlPoints);

case SplineType.PerfectCurve:
{
if (subControlPoints.Length != 3)
break;

Expand All @@ -305,17 +337,66 @@ 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);
}

private void calculateLength()
{
calculatedLength = 0;
calculatedLength = optimisedLength;
cumulativeLength.Clear();
cumulativeLength.Add(0);

Expand Down

0 comments on commit b559bec

Please sign in to comment.