From 6d579af447b45026d495c51bc363c055fca1e916 Mon Sep 17 00:00:00 2001 From: cs Date: Thu, 9 Nov 2023 07:25:19 +0100 Subject: [PATCH 1/6] Add class for efficiently building B-Splines This adds the IncrementalBSplineBuilder class which can be used to build up a B-Spline from a series of linear points efficiently. The class takes special care to keep the number of control points --- .../MathUtils/TestPathApproximator.cs | 4 +- .../TestSceneCircularArcBoundingBox.cs | 2 +- .../Drawables/TestSceneCustomEasingCurve.cs | 2 +- .../TestSceneInteractivePathDrawing.cs | 109 ++++++++++ .../Drawables/TestScenePathApproximator.cs | 28 +-- .../TestSceneShaderStorageBufferObject.cs | 2 +- osu.Framework/Graphics/Lines/Path.cs | 10 + .../Utils/IncrementalBSplineBuilder.cs | 190 ++++++++++++++++++ osu.Framework/Utils/PathApproximator.cs | 62 +++--- 9 files changed, 359 insertions(+), 50 deletions(-) create mode 100644 osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs create mode 100644 osu.Framework/Utils/IncrementalBSplineBuilder.cs diff --git a/osu.Framework.Tests/MathUtils/TestPathApproximator.cs b/osu.Framework.Tests/MathUtils/TestPathApproximator.cs index 8bf2db49c9..f545f42a29 100644 --- a/osu.Framework.Tests/MathUtils/TestPathApproximator.cs +++ b/osu.Framework.Tests/MathUtils/TestPathApproximator.cs @@ -17,7 +17,7 @@ public void TestLagrange() // lagrange of (0,0) (0.5,0.35) (1,1) is equal to 0.6x*x + 0.4x Vector2[] points = { new Vector2(0, 0), new Vector2(0.5f, 0.35f), new Vector2(1, 1) }; - List approximated = PathApproximator.ApproximateLagrangePolynomial(points); + List approximated = PathApproximator.LagrangePolynomialToPiecewiseLinear(points); Assert.Greater(approximated.Count, 10, "Approximated polynomial should have at least 10 points to test"); for (int i = 0; i < approximated.Count; i++) @@ -34,7 +34,7 @@ public void TestBSpline() { Vector2[] points = { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, -1), new Vector2(-1, -1), new Vector2(-1, 1), new Vector2(3, 2), new Vector2(3, 0) }; - List approximated = PathApproximator.ApproximateBSpline(points, 4); + List approximated = PathApproximator.BSplineToPiecewiseLinear(points, 4); Assert.AreEqual(approximated.Count, 29, "Approximated path should have 29 points to test"); Assert.True(Precision.AlmostEquals(approximated[0], points[0], 1e-4f)); Assert.True(Precision.AlmostEquals(approximated[28], points[6], 1e-4f)); diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneCircularArcBoundingBox.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneCircularArcBoundingBox.cs index 9617092306..bee820a296 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneCircularArcBoundingBox.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneCircularArcBoundingBox.cs @@ -74,7 +74,7 @@ protected override void LoadComplete() if (copy.Length != 3) return; - path.Vertices = PathApproximator.ApproximateCircularArc(copy); + path.Vertices = PathApproximator.CircularArcToPiecewiseLinear(copy); var bounds = PathApproximator.CircularArcBoundingBox(copy); boundingBox.Size = bounds.Size; diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneCustomEasingCurve.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneCustomEasingCurve.cs index 56c1438ea3..d7673038de 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneCustomEasingCurve.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneCustomEasingCurve.cs @@ -165,7 +165,7 @@ protected override void Update() vectorPath.AddRange(ordered.Select(p => p.PointPosition.Value)); vectorPath.Add(new Vector2(DrawWidth, 0)); - var bezierPath = PathApproximator.ApproximateBezier(vectorPath.ToArray()); + var bezierPath = PathApproximator.BezierToPiecewiseLinear(vectorPath.ToArray()); path.Vertices = bezierPath; path.Position = -path.PositionInBoundingBox(Vector2.Zero); diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs new file mode 100644 index 0000000000..cd3a679add --- /dev/null +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -0,0 +1,109 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Lines; +using osu.Framework.Input.Events; +using osuTK.Input; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Framework.Tests.Visual.Drawables +{ + [System.ComponentModel.Description("Approximate a hand-drawn path with minimal B-spline control points")] + public partial class TestSceneInteractivePathDrawing : FrameworkTestScene + { + private readonly Path rawDrawnPath; + private readonly Path approximatedDrawnPath; + private readonly Container controlPointViz; + + private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder(); + + public TestSceneInteractivePathDrawing() + { + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + rawDrawnPath = new Path + { + Colour = Color4.DeepPink, + PathRadius = 5, + }, + approximatedDrawnPath = new Path + { + Colour = Color4.Blue, + PathRadius = 3, + }, + controlPointViz = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.5f, + }, + } + }; + + updateViz(); + OnUpdate += _ => updateControlPointsViz(); + + AddStep("Reset path", () => + { + bSplineBuilder.Clear(); + }); + + AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 5, 3, v => + { + bSplineBuilder.Degree = v; + }); + AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 1f, 0.1f, v => + { + bSplineBuilder.Tolerance = v; + }); + } + + private void updateControlPointsViz() + { + controlPointViz.Clear(); + + foreach (var cp in bSplineBuilder.GetControlPoints()) + { + controlPointViz.Add(new Box + { + Origin = Anchor.Centre, + Size = new Vector2(10), + Position = cp, + Colour = Color4.LightGreen, + }); + } + } + + protected override bool OnDragStart(DragStartEvent e) + { + if (e.Button == MouseButton.Left) + { + bSplineBuilder.Clear(); + bSplineBuilder.AddLinearPoint(rawDrawnPath.ToLocalSpace(ToScreenSpace(e.MousePosition))); + return true; + } + + return false; + } + + private void updateViz() + { + rawDrawnPath.Vertices = bSplineBuilder.GetInputPath(); + approximatedDrawnPath.Vertices = bSplineBuilder.OutputPath; + + updateControlPointsViz(); + } + + protected override void OnDrag(DragEvent e) + { + bSplineBuilder.AddLinearPoint(rawDrawnPath.ToLocalSpace(ToScreenSpace(e.MousePosition))); + } + } +} diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs index f473b6903b..8822abd4cf 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs @@ -16,30 +16,30 @@ namespace osu.Framework.Tests.Visual.Drawables public partial class TestScenePathApproximator : GridTestScene { public TestScenePathApproximator() - : base(2, 2) + : base(2, 3) { Cell(0).AddRange(new[] { - createLabel("ApproximateBezier"), - new ApproximatedPathTest(PathApproximator.ApproximateBezier), + createLabel("BezierToPiecewiseLinear"), + new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), }); - Cell(1).AddRange(new[] + Cell(2).AddRange(new[] { - createLabel("ApproximateCatmull"), - new ApproximatedPathTest(PathApproximator.ApproximateCatmull), + createLabel("CatmullToPiecewiseLinear"), + new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), }); - Cell(2).AddRange(new[] + Cell(3).AddRange(new[] { - createLabel("ApproximateCircularArc"), - new ApproximatedPathTest(PathApproximator.ApproximateCircularArc), + createLabel("CircularArcToPiecewiseLinear"), + new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), }); - Cell(3).AddRange(new[] + Cell(4).AddRange(new[] { - createLabel("ApproximateLagrangePolynomial"), - new ApproximatedPathTest(PathApproximator.ApproximateLagrangePolynomial), + createLabel("LagrangePolynomialToPiecewiseLinear"), + new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), }); } @@ -50,10 +50,10 @@ public TestScenePathApproximator() Colour = Color4.White, }; + public delegate List ApproximatorFunc(ReadOnlySpan controlPoints); + private partial class ApproximatedPathTest : SmoothPath { - public delegate List ApproximatorFunc(ReadOnlySpan controlPoints); - public ApproximatedPathTest(ApproximatorFunc approximator) { Vector2[] points = new Vector2[5]; diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index 3086681f6d..3403902a34 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -123,7 +123,7 @@ public override void Draw(IRenderer renderer) shader.Bind(); shader.BindUniformBlock("g_ColourBuffer", colourBuffer); - // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. + // Submit rawDrawnPath, making sure that we don't submit an index which would overflow the SSBO. for (int i = 0; i < areas.Count; i++) { vertices.Add(new ColourIndexedVertex diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs index 509d88eb63..e0c502e760 100644 --- a/osu.Framework/Graphics/Lines/Path.cs +++ b/osu.Framework/Graphics/Lines/Path.cs @@ -237,6 +237,16 @@ public void AddVertex(Vector2 pos) Invalidate(Invalidation.DrawSize); } + public void ReplaceVertex(int index, Vector2 pos) + { + vertices[index] = pos; + + vertexBoundsCache.Invalidate(); + segmentsCache.Invalidate(); + + Invalidate(Invalidation.DrawSize); + } + private readonly List segmentsBacking = new List(); private readonly Cached segmentsCache = new Cached(); private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments(); diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs new file mode 100644 index 0000000000..0075678c36 --- /dev/null +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -0,0 +1,190 @@ +// Copyright (c) ppy Pty Ltd . 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.Diagnostics; +using System.Linq; +using osu.Framework.Caching; +using osuTK; + +namespace osu.Framework.Utils +{ + /// + /// A class for incrementally building B-Spline paths from a series of linear points. + /// This can be used to obtain a B-Spline with a minimal number of control points for any + /// given set of linear input points, for example, a hand-drawn path. + /// + public class IncrementalBSplineBuilder + { + private readonly List inputPath = new List(); + + private readonly Cached> outputCache = new Cached> + { + Value = new List() + }; + + private readonly List controlPoints = new List(); + + private int degree; + + /// + /// Gets or sets the degree of the B-Spline. Must not be negative. Default is 3. + /// + /// Thrown when the value is negative. + public int Degree + { + get => degree; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), "Degree must not be negative."); + + degree = value; + outputCache.Invalidate(); + } + } + + private float tolerance; + + /// + /// Gets or sets the tolerance for determining when to add a new control point. Must not be negative. Default is 0.1. + /// + /// Thrown when the value is negative. + public float Tolerance + { + get => tolerance; + set + { + if (tolerance < 0) + throw new ArgumentOutOfRangeException(nameof(value), "Tolerance must not be negative."); + + tolerance = value; + outputCache.Invalidate(); + } + } + + /// + /// The piecewise linear approximation of the B-spline created from the input path. + /// + public IReadOnlyList OutputPath + { + get + { + if (!outputCache.IsValid) + redrawApproximatedPath(); + + return outputCache.Value; + } + } + + /// + /// Initializes a new instance of the class with specified degree and tolerance. + /// + /// The degree of the B-Spline. + /// The tolerance for control point addition. + public IncrementalBSplineBuilder(int degree = 3, float tolerance = 0.1f) + { + Degree = degree; + Tolerance = tolerance; + } + + /// + /// The list of control points of the B-Spline. This is inferred from the input path. + /// + public IReadOnlyList GetControlPoints() + => controlPoints.ToArray(); + + /// + /// The list of input points. + /// + public IReadOnlyList GetInputPath() + => inputPath.ToArray(); + + private void redrawApproximatedPath() + { + // Set value of output cache to update the cache to be valid. + outputCache.Value = new List(); + if (inputPath.Count == 0) + return; + + var oldInputs = inputPath.ToList(); + + inputPath.Clear(); + controlPoints.Clear(); + + foreach (var v in oldInputs) + AddLinearPoint(v); + } + + /// + /// Clears the input path and the B-Spline. + /// + public void Clear() + { + inputPath.Clear(); + controlPoints.Clear(); + if (outputCache.IsValid) + outputCache.Value.Clear(); + } + + /// + /// Adds a linear point to the path and updates the B-Spline accordingly. + /// + /// The vector representing the point to add. + public void AddLinearPoint(Vector2 v) + { + if (!outputCache.IsValid) + redrawApproximatedPath(); + + if (inputPath.Count == 0) + { + inputPath.Add(v); + controlPoints.Add(inputPath[0]); + controlPoints.Add(inputPath[0]); + return; + } + + inputPath.Add(v); + + var cps = controlPoints; + Debug.Assert(cps.Count >= 2); + + cps[^1] = inputPath[^1]; + + // Calculate the normalized momentum vectors for both raw and approximated paths. + // Momentum here refers to a direction vector representing the path's direction of movement. + var mraw = momentumDirection(inputPath, 3); + var mcp = cps.Count > 2 ? momentumDirection(outputCache.Value, 1) : Vector2.Zero; + + // Determine the alignment between the raw path and control path's momentums. + // It uses Vector2.Dot which calculates the cosine of the angle between two vectors. + // This alignment is used to adjust the control points based on the path's direction change. + float alignment = MathF.Max(Vector2.Dot(mraw, mcp), 0.01f); + + // Calculate the distance between the last two control points. + // This distance is then used, along with alignment, to decide if a new control point is needed. + // The threshold for adding a new control point is based on the alignment and a predefined accuracy factor. + float distance = Vector2.Distance(cps[^1], cps[^2]); + if (distance / MathF.Pow(alignment, 4) > Tolerance * 1000) + cps.Add(cps[^1]); + + outputCache.Value = PathApproximator.BSplineToPiecewiseLinear(cps.ToArray(), degree); + } + + private Vector2 momentumDirection(IReadOnlyList vertices, int window) + { + if (vertices.Count < window + 1) + return Vector2.Zero; + + var sum = Vector2.Zero; + for (int i = 0; i < window; i++) + sum += vertices[^(i + 1)] - vertices[^(i + 2)]; + + if (Precision.AlmostEquals(sum.X, 0, 1e-7f) && Precision.AlmostEquals(sum.Y, 0, 1e-7f)) + return Vector2.Zero; + + return (sum / window).Normalized(); + } + } +} diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 470a841342..7ad8d420f9 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -29,27 +29,27 @@ public static class PathApproximator /// /// The control points. /// A list of vectors representing the piecewise-linear approximation. - public static List ApproximateBezier(ReadOnlySpan controlPoints) + public static List BezierToPiecewiseLinear(ReadOnlySpan controlPoints) { - return ApproximateBSpline(controlPoints); + return BSplineToPiecewiseLinear(controlPoints, 0); } /// - /// Creates a piecewise-linear approximation of a clamped uniform B-spline with polynomial order p, + /// Creates a piecewise-linear approximation of a clamped uniform B-spline with polynomial order degree, /// by dividing it into a series of bezier control points at its knots, then adaptively repeatedly /// subdividing those until their approximation error vanishes below a given threshold. - /// Retains previous bezier approximation functionality when p is 0 or too large to create knots. - /// Algorithm unsuitable for large values of p with many knots. + /// Retains previous bezier approximation functionality when degree is 0 or too large to create knots. + /// Algorithm unsuitable for large values of degree with many knots. /// /// The control points. - /// The polynomial order. + /// The polynomial order. /// A list of vectors representing the piecewise-linear approximation. - public static List ApproximateBSpline(ReadOnlySpan controlPoints, int p = 0) + public static List BSplineToPiecewiseLinear(ReadOnlySpan controlPoints, int degree) { List output = new List(); - int n = controlPoints.Length - 1; + int nPoints = controlPoints.Length - 1; - if (n < 0) + if (nPoints < 0) return output; Stack toFlatten = new Stack(); @@ -57,38 +57,38 @@ public static List ApproximateBSpline(ReadOnlySpan controlPoin var points = controlPoints.ToArray(); - if (p > 0 && p < n) + if (degree > 0 && degree < nPoints) { // Subdivide B-spline into bezier control points at knots. - for (int i = 0; i < n - p; i++) + for (int i = 0; i < nPoints - degree; i++) { - var subBezier = new Vector2[p + 1]; + var subBezier = new Vector2[degree + 1]; subBezier[0] = points[i]; - // Destructively insert the knot p-1 times via Boehm's algorithm. - for (int j = 0; j < p - 1; j++) + // Destructively insert the knot degree-1 times via Boehm's algorithm. + for (int j = 0; j < degree - 1; j++) { subBezier[j + 1] = points[i + 1]; - for (int k = 1; k < p - j; k++) + for (int k = 1; k < degree - j; k++) { - int l = Math.Min(k, n - p - i); + int l = Math.Min(k, nPoints - degree - i); points[i + k] = (l * points[i + k] + points[i + k + 1]) / (l + 1); } } - subBezier[p] = points[i + 1]; + subBezier[degree] = points[i + 1]; toFlatten.Push(subBezier); } - toFlatten.Push(points[(n - p)..]); + toFlatten.Push(points[(nPoints - degree)..]); // Reverse the stack so elements can be accessed in order. toFlatten = new Stack(toFlatten); } else { // B-spline subdivision unnecessary, degenerate to single bezier. - p = n; + degree = nPoints; toFlatten.Push(points); } // "toFlatten" contains all the curves which are not yet approximated well enough. @@ -97,8 +97,8 @@ public static List ApproximateBSpline(ReadOnlySpan controlPoin // Depth-first search // over the tree resulting from the subdivisions we make.) - var subdivisionBuffer1 = new Vector2[p + 1]; - var subdivisionBuffer2 = new Vector2[p * 2 + 1]; + var subdivisionBuffer1 = new Vector2[degree + 1]; + var subdivisionBuffer2 = new Vector2[degree * 2 + 1]; Vector2[] leftChild = subdivisionBuffer2; @@ -112,7 +112,7 @@ public static List ApproximateBSpline(ReadOnlySpan controlPoin // 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. - bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, p + 1); + bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, degree + 1); freeBuffers.Push(parent); continue; @@ -120,18 +120,18 @@ public static List ApproximateBSpline(ReadOnlySpan controlPoin // 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[p + 1]; - bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, p + 1); + Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[degree + 1]; + bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, degree + 1); // 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 < p + 1; ++i) + for (int i = 0; i < degree + 1; ++i) parent[i] = leftChild[i]; toFlatten.Push(rightChild); toFlatten.Push(parent); } - output.Add(controlPoints[n]); + output.Add(controlPoints[nPoints]); return output; } @@ -139,7 +139,7 @@ public static List ApproximateBSpline(ReadOnlySpan controlPoin /// Creates a piecewise-linear approximation of a Catmull-Rom spline. /// /// A list of vectors representing the piecewise-linear approximation. - public static List ApproximateCatmull(ReadOnlySpan controlPoints) + public static List CatmullToPiecewiseLinear(ReadOnlySpan controlPoints) { var result = new List((controlPoints.Length - 1) * catmull_detail * 2); @@ -164,11 +164,11 @@ public static List ApproximateCatmull(ReadOnlySpan controlPoin /// Creates a piecewise-linear approximation of a circular arc curve. /// /// A list of vectors representing the piecewise-linear approximation. - public static List ApproximateCircularArc(ReadOnlySpan controlPoints) + public static List CircularArcToPiecewiseLinear(ReadOnlySpan controlPoints) { CircularArcProperties pr = new CircularArcProperties(controlPoints); if (!pr.IsValid) - return ApproximateBezier(controlPoints); + return BezierToPiecewiseLinear(controlPoints); // 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 @@ -244,7 +244,7 @@ public static RectangleF CircularArcBoundingBox(ReadOnlySpan controlPoi /// Basically, returns the input. /// /// A list of vectors representing the piecewise-linear approximation. - public static List ApproximateLinear(ReadOnlySpan controlPoints) + public static List LinearToPiecewiseLinear(ReadOnlySpan controlPoints) { var result = new List(controlPoints.Length); @@ -258,7 +258,7 @@ public static List ApproximateLinear(ReadOnlySpan controlPoint /// Creates a piecewise-linear approximation of a lagrange polynomial. /// /// A list of vectors representing the piecewise-linear approximation. - public static List ApproximateLagrangePolynomial(ReadOnlySpan controlPoints) + public static List LagrangePolynomialToPiecewiseLinear(ReadOnlySpan controlPoints) { // TODO: add some smarter logic here, chebyshev nodes? const int num_steps = 51; From c93a24fa13f973d193234d5bfe9b7c40c2b12ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Sat, 11 Nov 2023 19:00:14 +0900 Subject: [PATCH 2/6] TestSceneInteractivePathDrawing: fix missing path viz --- .../Visual/Drawables/TestSceneInteractivePathDrawing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs index cd3a679add..4ebe43eca6 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs @@ -48,7 +48,7 @@ public TestSceneInteractivePathDrawing() }; updateViz(); - OnUpdate += _ => updateControlPointsViz(); + OnUpdate += _ => updateViz(); AddStep("Reset path", () => { From a173354ddcf83d5b0329da6e76aa71e409c3f05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Mu=CC=88ller-Ho=CC=88hne?= Date: Sat, 11 Nov 2023 23:30:04 +0900 Subject: [PATCH 3/6] IncrementalBSplineBuilder: fix doubled-up control point at beginning --- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index 0075678c36..ae3de41bef 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -160,7 +160,7 @@ public void AddLinearPoint(Vector2 v) // Determine the alignment between the raw path and control path's momentums. // It uses Vector2.Dot which calculates the cosine of the angle between two vectors. // This alignment is used to adjust the control points based on the path's direction change. - float alignment = MathF.Max(Vector2.Dot(mraw, mcp), 0.01f); + float alignment = mraw == Vector2.Zero || mcp == Vector2.Zero ? 1.0f : MathF.Max(Vector2.Dot(mraw, mcp), 0.01f); // Calculate the distance between the last two control points. // This distance is then used, along with alignment, to decide if a new control point is needed. From 3939d21f707f0a4231c4c20554340c1195a14d8a Mon Sep 17 00:00:00 2001 From: cs Date: Sun, 12 Nov 2023 07:43:49 +0100 Subject: [PATCH 4/6] Fixes and corrections from review --- .../Drawables/TestScenePathApproximator.cs | 2 +- .../TestSceneShaderStorageBufferObject.cs | 2 +- osu.Framework/Utils/IncrementalBSplineBuilder.cs | 7 +++++-- osu.Framework/Utils/PathApproximator.cs | 16 ++++++++-------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs index 8822abd4cf..a3e01aa5fe 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs @@ -16,7 +16,7 @@ namespace osu.Framework.Tests.Visual.Drawables public partial class TestScenePathApproximator : GridTestScene { public TestScenePathApproximator() - : base(2, 3) + : base(2, 2) { Cell(0).AddRange(new[] { diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index 3403902a34..3086681f6d 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -123,7 +123,7 @@ public override void Draw(IRenderer renderer) shader.Bind(); shader.BindUniformBlock("g_ColourBuffer", colourBuffer); - // Submit rawDrawnPath, making sure that we don't submit an index which would overflow the SSBO. + // Submit vertices, making sure that we don't submit an index which would overflow the SSBO. for (int i = 0; i < areas.Count; i++) { vertices.Add(new ColourIndexedVertex diff --git a/osu.Framework/Utils/IncrementalBSplineBuilder.cs b/osu.Framework/Utils/IncrementalBSplineBuilder.cs index ae3de41bef..1f4b9eef39 100644 --- a/osu.Framework/Utils/IncrementalBSplineBuilder.cs +++ b/osu.Framework/Utils/IncrementalBSplineBuilder.cs @@ -124,8 +124,7 @@ public void Clear() { inputPath.Clear(); controlPoints.Clear(); - if (outputCache.IsValid) - outputCache.Value.Clear(); + outputCache.Value = new List(); } /// @@ -140,6 +139,8 @@ public void AddLinearPoint(Vector2 v) if (inputPath.Count == 0) { inputPath.Add(v); + // We add the first point twice so we can track the + // end point of the raw path using the last control point. controlPoints.Add(inputPath[0]); controlPoints.Add(inputPath[0]); return; @@ -181,6 +182,8 @@ private Vector2 momentumDirection(IReadOnlyList vertices, int window) for (int i = 0; i < window; i++) sum += vertices[^(i + 1)] - vertices[^(i + 2)]; + // We choose a very small acceptable difference here to ensure that + // small momenta are not ignored. if (Precision.AlmostEquals(sum.X, 0, 1e-7f) && Precision.AlmostEquals(sum.Y, 0, 1e-7f)) return Vector2.Zero; diff --git a/osu.Framework/Utils/PathApproximator.cs b/osu.Framework/Utils/PathApproximator.cs index 7ad8d420f9..261d00499d 100644 --- a/osu.Framework/Utils/PathApproximator.cs +++ b/osu.Framework/Utils/PathApproximator.cs @@ -47,9 +47,9 @@ public static List BezierToPiecewiseLinear(ReadOnlySpan contro public static List BSplineToPiecewiseLinear(ReadOnlySpan controlPoints, int degree) { List output = new List(); - int nPoints = controlPoints.Length - 1; + int pointCount = controlPoints.Length - 1; - if (nPoints < 0) + if (pointCount < 0) return output; Stack toFlatten = new Stack(); @@ -57,10 +57,10 @@ public static List BSplineToPiecewiseLinear(ReadOnlySpan contr var points = controlPoints.ToArray(); - if (degree > 0 && degree < nPoints) + if (degree > 0 && degree < pointCount) { // Subdivide B-spline into bezier control points at knots. - for (int i = 0; i < nPoints - degree; i++) + for (int i = 0; i < pointCount - degree; i++) { var subBezier = new Vector2[degree + 1]; subBezier[0] = points[i]; @@ -72,7 +72,7 @@ public static List BSplineToPiecewiseLinear(ReadOnlySpan contr for (int k = 1; k < degree - j; k++) { - int l = Math.Min(k, nPoints - degree - i); + int l = Math.Min(k, pointCount - degree - i); points[i + k] = (l * points[i + k] + points[i + k + 1]) / (l + 1); } } @@ -81,14 +81,14 @@ public static List BSplineToPiecewiseLinear(ReadOnlySpan contr toFlatten.Push(subBezier); } - toFlatten.Push(points[(nPoints - degree)..]); + toFlatten.Push(points[(pointCount - degree)..]); // Reverse the stack so elements can be accessed in order. toFlatten = new Stack(toFlatten); } else { // B-spline subdivision unnecessary, degenerate to single bezier. - degree = nPoints; + degree = pointCount; toFlatten.Push(points); } // "toFlatten" contains all the curves which are not yet approximated well enough. @@ -131,7 +131,7 @@ public static List BSplineToPiecewiseLinear(ReadOnlySpan contr toFlatten.Push(parent); } - output.Add(controlPoints[nPoints]); + output.Add(controlPoints[pointCount]); return output; } From 6dedbc43bdc4b296936d50a06b4a9f1be697d642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 12 Nov 2023 15:48:51 +0900 Subject: [PATCH 5/6] Fix off-by-one in test cell indices --- .../Visual/Drawables/TestScenePathApproximator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs index a3e01aa5fe..74807e8811 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs @@ -24,19 +24,19 @@ public TestScenePathApproximator() new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), }); - Cell(2).AddRange(new[] + Cell(1).AddRange(new[] { createLabel("CatmullToPiecewiseLinear"), new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), }); - Cell(3).AddRange(new[] + Cell(2).AddRange(new[] { createLabel("CircularArcToPiecewiseLinear"), new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), }); - Cell(4).AddRange(new[] + Cell(3).AddRange(new[] { createLabel("LagrangePolynomialToPiecewiseLinear"), new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), From ef74d57e156488c0a478a1a6a209ac72820433ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 12 Nov 2023 15:52:03 +0900 Subject: [PATCH 6/6] Use `nameof` instead of hardcoded method names Slightly nicer. --- .../Visual/Drawables/TestScenePathApproximator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs index 74807e8811..5deed3cb67 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestScenePathApproximator.cs @@ -20,25 +20,25 @@ public TestScenePathApproximator() { Cell(0).AddRange(new[] { - createLabel("BezierToPiecewiseLinear"), + createLabel(nameof(PathApproximator.BezierToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.BezierToPiecewiseLinear), }); Cell(1).AddRange(new[] { - createLabel("CatmullToPiecewiseLinear"), + createLabel(nameof(PathApproximator.CatmullToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.CatmullToPiecewiseLinear), }); Cell(2).AddRange(new[] { - createLabel("CircularArcToPiecewiseLinear"), + createLabel(nameof(PathApproximator.CircularArcToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.CircularArcToPiecewiseLinear), }); Cell(3).AddRange(new[] { - createLabel("LagrangePolynomialToPiecewiseLinear"), + createLabel(nameof(PathApproximator.LagrangePolynomialToPiecewiseLinear)), new ApproximatedPathTest(PathApproximator.LagrangePolynomialToPiecewiseLinear), }); }