1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 17:27:24 +08:00

Merge pull request #20952 from OliBomby/bezier-convert

Add conversion algorithm for lazer slider curves to work with stable
This commit is contained in:
Dean Herbert 2022-11-04 16:49:51 +09:00 committed by GitHub
commit 566738494f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 477 additions and 0 deletions

View File

@ -0,0 +1,190 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneBezierConverter : OsuTestScene
{
private readonly SmoothPath drawablePath;
private readonly SmoothPath controlPointDrawablePath;
private readonly SmoothPath convertedDrawablePath;
private readonly SmoothPath convertedControlPointDrawablePath;
private SliderPath path = null!;
private SliderPath convertedPath = null!;
public TestSceneBezierConverter()
{
Children = new Drawable[]
{
new Container
{
Children =
new Drawable[]
{
drawablePath = new SmoothPath(),
controlPointDrawablePath = new SmoothPath
{
Colour = Colour4.Magenta,
PathRadius = 1f
}
},
Position = new Vector2(100)
},
new Container
{
Children =
new Drawable[]
{
convertedDrawablePath = new SmoothPath(),
convertedControlPointDrawablePath = new SmoothPath
{
Colour = Colour4.Magenta,
PathRadius = 1f
}
},
Position = new Vector2(100, 300)
}
};
resetPath();
}
[SetUp]
public void Setup() => Schedule(resetPath);
private void resetPath()
{
path = new SliderPath();
convertedPath = new SliderPath();
path.Version.ValueChanged += getConvertedControlPoints;
}
private void getConvertedControlPoints(ValueChangedEvent<int> obj)
{
convertedPath.ControlPoints.Clear();
convertedPath.ControlPoints.AddRange(BezierConverter.ConvertToModernBezier(path.ControlPoints));
}
protected override void Update()
{
base.Update();
List<Vector2> vertices = new List<Vector2>();
path.GetPathToProgress(vertices, 0, 1);
drawablePath.Vertices = vertices;
controlPointDrawablePath.Vertices = path.ControlPoints.Select(o => o.Position).ToList();
if (controlPointDrawablePath.Vertices.Count > 0)
{
controlPointDrawablePath.Position =
drawablePath.PositionInBoundingBox(drawablePath.Vertices[0]) - controlPointDrawablePath.PositionInBoundingBox(controlPointDrawablePath.Vertices[0]);
}
vertices.Clear();
convertedPath.GetPathToProgress(vertices, 0, 1);
convertedDrawablePath.Vertices = vertices;
convertedControlPointDrawablePath.Vertices = convertedPath.ControlPoints.Select(o => o.Position).ToList();
if (convertedControlPointDrawablePath.Vertices.Count > 0)
{
convertedControlPointDrawablePath.Position = convertedDrawablePath.PositionInBoundingBox(convertedDrawablePath.Vertices[0])
- convertedControlPointDrawablePath.PositionInBoundingBox(convertedControlPointDrawablePath.Vertices[0]);
}
}
[Test]
public void TestEmptyPath()
{
}
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestSingleSegment(PathType type)
=> AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
[TestCase(PathType.Linear)]
[TestCase(PathType.Bezier)]
[TestCase(PathType.Catmull)]
[TestCase(PathType.PerfectCurve)]
public void TestMultipleSegment(PathType type)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
});
}
[Test]
public void TestComplex()
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0)));
path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
});
}
[TestCase(0, 100)]
[TestCase(1, 100)]
[TestCase(5, 100)]
[TestCase(10, 100)]
[TestCase(30, 100)]
[TestCase(50, 100)]
[TestCase(100, 100)]
[TestCase(100, 1)]
public void TestPerfectCurveAngles(float height, float width)
{
AddStep("create path", () =>
{
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
});
}
[TestCase(2)]
[TestCase(4)]
public void TestPerfectCurveFallbackScenarios(int points)
{
AddStep("create path", () =>
{
switch (points)
{
case 2:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
break;
case 4:
path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
break;
}
});
}
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
{
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
points[0].Type = type;
return points;
}
}
}

View File

@ -0,0 +1,287 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects
{
public static class BezierConverter
{
private struct CircleBezierPreset
{
public readonly double ArcLength;
public readonly Vector2d[] ControlPoints;
public CircleBezierPreset(double arcLength, Vector2d[] controlPoints)
{
ArcLength = arcLength;
ControlPoints = controlPoints;
}
}
// Extremely accurate a bezier anchor positions for approximating circles of several arc lengths
private static readonly CircleBezierPreset[] circle_presets =
{
new CircleBezierPreset(0.4993379862754501,
new[] { new Vector2d(1, 0), new Vector2d(1, 0.2549893626632736f), new Vector2d(0.8778997558480327f, 0.47884446188920726f) }),
new CircleBezierPreset(1.7579419829169447,
new[] { new Vector2d(1, 0), new Vector2d(1, 0.6263026f), new Vector2d(0.42931178f, 1.0990661f), new Vector2d(-0.18605515f, 0.9825393f) }),
new CircleBezierPreset(3.1385246920140215,
new[] { new Vector2d(1, 0), new Vector2d(1, 0.87084764f), new Vector2d(0.002304826f, 1.5033062f), new Vector2d(-0.9973236f, 0.8739115f), new Vector2d(-0.9999953f, 0.0030679568f) }),
new CircleBezierPreset(5.69720464620727,
new[] { new Vector2d(1, 0), new Vector2d(1, 1.4137783f), new Vector2d(-1.4305235f, 2.0779421f), new Vector2d(-2.3410065f, -0.94017583f), new Vector2d(0.05132711f, -1.7309346f), new Vector2d(0.8331702f, -0.5530167f) }),
new CircleBezierPreset(2 * Math.PI,
new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) })
};
/// <summary>
/// Converts a slider path to bezier control point positions compatible with the legacy osu! client.
/// </summary>
/// <param name="controlPoints">The control points of the path.</param>
/// <param name="position">The offset for the whole path.</param>
/// <returns>The list of legacy bezier control point positions.</returns>
public static List<Vector2> ConvertToLegacyBezier(IList<PathControlPoint> controlPoints, Vector2 position)
{
Vector2[] vertices = new Vector2[controlPoints.Count];
for (int i = 0; i < controlPoints.Count; i++)
vertices[i] = controlPoints[i].Position;
var result = new List<Vector2>();
int start = 0;
for (int i = 0; i < controlPoints.Count; i++)
{
if (controlPoints[i].Type == null && i < controlPoints.Count - 1)
continue;
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = controlPoints[start].Type ?? PathType.Linear;
switch (segmentType)
{
case PathType.Catmull:
result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position);
break;
case PathType.Linear:
result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position);
break;
case PathType.PerfectCurve:
result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position));
break;
default:
foreach (Vector2 v in segmentVertices)
{
result.Add(v + position);
}
break;
}
// Start the new segment at the current vertex
start = i;
}
return result;
}
/// <summary>
/// Converts a path of control points to an identical path using only Bezier type control points.
/// </summary>
/// <param name="controlPoints">The control points of the path.</param>
/// <returns>The list of bezier control points.</returns>
public static List<PathControlPoint> ConvertToModernBezier(IList<PathControlPoint> controlPoints)
{
Vector2[] vertices = new Vector2[controlPoints.Count];
for (int i = 0; i < controlPoints.Count; i++)
vertices[i] = controlPoints[i].Position;
var result = new List<PathControlPoint>();
int start = 0;
for (int i = 0; i < controlPoints.Count; i++)
{
if (controlPoints[i].Type == null && i < controlPoints.Count - 1)
continue;
// The current vertex ends the segment
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
var segmentType = controlPoints[start].Type ?? PathType.Linear;
switch (segmentType)
{
case PathType.Catmull:
foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices))
{
for (int j = 0; j < segment.Length - 1; j++)
{
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
}
}
break;
case PathType.Linear:
foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices))
{
for (int j = 0; j < segment.Length - 1; j++)
{
result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
}
}
break;
case PathType.PerfectCurve:
var circleResult = ConvertCircleToBezierAnchors(segmentVertices);
for (int j = 0; j < circleResult.Length - 1; j++)
{
result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null));
}
break;
default:
for (int j = 0; j < segmentVertices.Length - 1; j++)
{
result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null));
}
break;
}
// Start the new segment at the current vertex
start = i;
}
result.Add(new PathControlPoint(controlPoints[^1].Position));
return result;
}
/// <summary>
/// Converts perfect curve anchors to bezier anchors.
/// </summary>
/// <param name="controlPoints">The control point positions to convert.</param>
public static Vector2[] ConvertCircleToBezierAnchors(ReadOnlySpan<Vector2> controlPoints)
{
if (controlPoints.Length != 3)
return controlPoints.ToArray();
var pr = new CircularArcProperties(controlPoints);
if (!pr.IsValid)
return controlPoints.ToArray();
CircleBezierPreset preset = circle_presets.Last();
foreach (CircleBezierPreset cbp in circle_presets)
{
if (cbp.ArcLength < pr.ThetaRange) continue;
preset = cbp;
break;
}
double arcLength = preset.ArcLength;
var arc = new Vector2d[preset.ControlPoints.Length];
preset.ControlPoints.CopyTo(arc, 0);
// Converge on arcLength of thetaRange
int n = arc.Length - 1;
double tf = pr.ThetaRange / arcLength;
while (Math.Abs(tf - 1) > 1E-7)
{
for (int j = 0; j < n; j++)
{
for (int i = n; i > j; i--)
{
arc[i] = arc[i] * tf + arc[i - 1] * (1 - tf);
}
}
arcLength = Math.Atan2(arc.Last()[1], arc.Last()[0]);
if (arcLength < 0)
{
arcLength += 2 * Math.PI;
}
tf = pr.ThetaRange / arcLength;
}
// Adjust rotation, radius, and position
var result = new Vector2[arc.Length];
for (int i = 0; i < arc.Length; i++)
{
result[i] = new Vector2(
(float)((Math.Cos(pr.ThetaStart) * arc[i].X + -Math.Sin(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.X),
(float)((Math.Sin(pr.ThetaStart) * arc[i].X + Math.Cos(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.Y));
}
return result;
}
/// <summary>
/// Converts catmull anchors to bezier anchors.
/// </summary>
/// <param name="controlPoints">The control point positions to convert.</param>
public static Vector2[][] ConvertCatmullToBezierAnchors(ReadOnlySpan<Vector2> controlPoints)
{
int iLen = controlPoints.Length;
var bezier = new Vector2[iLen - 1][];
for (int i = 0; i < iLen - 1; i++)
{
var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];
var v2 = controlPoints[i];
var v3 = i < iLen - 1 ? controlPoints[i + 1] : v2 + v2 - v1;
var v4 = i < iLen - 2 ? controlPoints[i + 2] : v3 + v3 - v2;
bezier[i] = new[]
{
v2,
(-v1 + 6 * v2 + v3) / 6,
(-v4 + 6 * v3 + v2) / 6,
v3
};
}
return bezier;
}
/// <summary>
/// Converts linear anchors to bezier anchors.
/// </summary>
/// <param name="controlPoints">The control point positions to convert.</param>
public static Vector2[][] ConvertLinearToBezierAnchors(ReadOnlySpan<Vector2> controlPoints)
{
int iLen = controlPoints.Length;
var bezier = new Vector2[iLen - 1][];
for (int i = 0; i < iLen - 1; i++)
{
bezier[i] = new[]
{
controlPoints[i],
controlPoints[i + 1]
};
}
return bezier;
}
}
}