-
Notifications
You must be signed in to change notification settings - Fork 420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add method to convert piecewise linear curves to spline control points #6056
Conversation
I had mentioned in #6044 (comment) that I'd open this PR. It's mostly meant for @Tom94 to compare the utility this algorithm provides. You may ignore reviewing code for now because I'll probably have to rewrite most of it anyways. |
Hi, I find the path approximations from the video very impressive for the relatively small amount of code used in the reconstruction! Thanks a bunch for the follow-up. I'd say this PR is orthogonal to the existing BSpline builder. We should probably keep the existing hard corner detection while being able to either drop-in replace the winding-based control point placement in between corners or use it as initialization for this algorithm. I feel like it'd be valuable to have this in framework in any case as something like a self-contained As you already point out, there are a few rough edges, but I think they can be ironed out. My main gripes would be:
|
Not looking too deep into this, but just want to add that this is a blocker in terms of osu! side usability. Control points generated by all settings exposed to the user must look sane to the user so they can easily edit/tweak the curve after drawing it. |
remove tensor.net and implement with pure c#
I've removed the changes to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code is underdocumented and opaque
/// <returns>Matrix array of B-spline basis function values.</returns> | ||
private static float[,] generateBSplineWeights(int numControlPoints, int numTestPoints, int degree) | ||
{ | ||
// Calculate the basis function values using a modified De Boor's algorithm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modified how and why? Is there a citation for this?
(I'm not even attempting to check the correctness of this implementation until this review is responded to.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this comment is not accurate. I didn't use any specific algorithm as reference.
I started with the Cox-de Boor recursion formula and turned it into my own algorithm by applying some optimizations. First I used dynamic programming to remove the recursion, and then I realized some steps can be ignored because the knots on the edges are all zero width.
What I ended up with looked pretty similar to De Boor's algorithm so I added that comment to point to some resources which might explain what's going on here.
I should change the comment to say that this method is just using Cox-de Boor recursion formula, but highly optimized.
I'd personally like to get this in with minimal review overhead (along with #6066 / ppy/osu#25658). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still think this is opaque in places but since i'm getting told to move this on -
|
||
for (int i = 0; i < numTestPoints; i++) | ||
{ | ||
prevOrder[i, (int)MathHelper.Clamp(x[i] * (numControlPoints - degree), 0, numControlPoints - degree - 1)] = 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why the upper range of the clamp is numControlPoints - degree - 1
, but changing it to what I would consider more correct breaks things, so there must be a reason for this that I'm not understanding...
The reason why I was suspicious is that while the left-side clamp looks effectively dead (because x[i]
should never be less than 0), the right-side clamp looks like it may cut off values on the right side. (the last one, specifically; x[^1] == 1
, and as such, numControlPoints - degree
will get clamped).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the Cox-de Boor recursion formula, the first order is basically sorting this x[i]
into a knot whose range includes x[i]
. Because this is an open uniform knot vector, we got degree
number of zero-width knots on the end-points and numControlPoints - degree
number of equally spaced knots in the middle. The knots range is usually not inclusive on the right side but in this case i want the last non zero-width knot to be inclusive on the right side, so the non-zero width knots include the whole range [0, 1]. This is exactly what the clamp achieves here.
// This code multiplies the previous order by equal length arrays of alphas and betas, | ||
// then shifts the alpha array by one index, and adds the results, resulting in one extra length. | ||
// nextOrder = (prevOrder * alphas).shiftRight() + (prevOrder * betas) | ||
float prevAlpha = 0; | ||
|
||
for (int j = 0; j < numControlPoints - degree + q - 1; j++) | ||
{ | ||
float alpha = (x[i] - knots[degree - q + 1 + j]) / (knots[degree + 1 + j] - knots[degree - q + 1 + j]); | ||
float alphaVal = alpha * prevOrder[i, j]; | ||
float betaVal = (1 - alpha) * prevOrder[i, j]; | ||
prevOrder[i, j] = prevAlpha + betaVal; | ||
prevAlpha = alphaVal; | ||
} | ||
|
||
prevOrder[i, numControlPoints - degree + q - 1] = prevAlpha; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Zero citations, zero anything. I basically don't know where any of this comes from, but since I'm told to keep review minimal, I suppose I will just look away.
matmul(controlPoints, weights, points); | ||
|
||
// Update labels to shift the distance distribution between points | ||
if (step % 11 == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
random "every eleventh step" (why eleventh?)
matLerp(grad, m, b1, m); | ||
matProduct(grad, grad, grad); | ||
matLerp(grad, v, b2, v); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that matLerp()
mixes the first operand with weight 1 - t
and the second with weight t
makes this so confusing to read....... When cross-referencing against the adam paper I almost thought this had it backwards for a second.
Summary
This PR introduces an algorithm for approximating arbitrary piecewise linear paths with B-splines or Bezier curves. The method
PiecewiseLinearToBSpline
is then used to improve the paths generated byIncrementalBSplineBuilder
, making the curve accurately follow the input path using as few control points as possible.PiecewiseLinearToBezier
andPiecewiseLinearToBSpline
toPathApproximator
.TestScenePiecewiseLinearToBSpline
to interactively test the algorithm on various curves with controllable parameters.BenchmarkPiecewiseLinearToBezier
to benchmark the performance of the algorithm.Utility
This addresses two points of concern mentioned in ppy/osu#25409:
The generated path more closely follows your input, so it should be easier to create the shape you are actually trying to create because you dont have to manually account for the inward bias.
The path requires less control points to still follow your input, so there are less control points to require tweaking afterwards.
TODOs
A later PR will introduce interop with
IncrementalBSplineBuilder
. I plan to improve performance by re-using results from previous frames so fewer iterations are necessary per frame to optimize the control points (this also would decrease wobbling).Also weight matrices can be cached in between frames for even more performance gains. I also want to use the total winding to increase precision in the approximation algorithm when necessary (on very long, complex paths).
Usability can be improved by locking previous control points which have been sufficiently optimised, so there is no wobble in the entire path while drawing the path.
Benchmarks
I added a nuget package to handle efficient large matrix multiplication, for this I added Tensor.NET. I chose this with performance in mind. I added a benchmark and compared performance against NumSharp, where Tensor.NET won by a large margin. Then I rewrote it to pure C# because Tensor.NET is not compatible and it got way more performant. Super large matrices might still be faster in Tensor.NET but that wont be the case most of the time.
Benchmark using NumSharp:
Benchmark using Tensor.NET (about 10x faster and comparable to native numpy in python):
Benchmark using pure C# and optimized allocations:
Benchmark using
System.Numerics.Tensors
for SIMD:@Tom94 Please give feedback, what do you think about the results?
osu.Framework.Tests_I2au6DMQHJ.mp4