-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Optimized partial and smooth Bézier curve computations in bezier.py
#3281
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
Optimized partial and smooth Bézier curve computations in bezier.py
#3281
Conversation
…ptimized_partial_smooth_beziers Pull from main to see if there are merge conflicts
…ial_quadratic_bezier_points
bezier.py
Hello everyone! It's been a long time.
|
Update:
|
…ptimized_partial_smooth_beziers
@overload | ||
def bezier( | ||
points: BezierPoints, | ||
) -> Callable[[float | ColVector], Point3D | Point3D_Array]: ... |
Check notice
Code scanning / CodeQL
Statement has no effect
) -> Callable[[float], Point3D]: | ||
"""Classic implementation of a bezier curve. | ||
points: Sequence[Point3D_Array], | ||
) -> Callable[[float | ColVector], Point3D_Array]: ... |
Check notice
Code scanning / CodeQL
Statement has no effect
@@ -265,36 +1001,69 @@ | |||
def interpolate(start: float, end: float, alpha: float) -> float: ... | |||
|
|||
|
|||
@overload | |||
def interpolate(start: float, end: float, alpha: ColVector) -> ColVector: ... |
Check notice
Code scanning / CodeQL
Statement has no effect
) -> float | Point3D: | ||
return (1 - alpha) * start + alpha * end | ||
@overload | ||
def interpolate(start: Point3D, end: Point3D, alpha: ColVector) -> Point3D_Array: ... |
Check notice
Code scanning / CodeQL
Statement has no effect
h0: Point3D, | ||
h1: Point3D, | ||
a1: Point3D, | ||
) -> Point3D_Array: ... |
Check notice
Code scanning / CodeQL
Statement has no effect
h0: Point3D_Array, | ||
h1: Point3D_Array, | ||
a1: Point3D_Array, | ||
) -> Point3D_Array: ... |
Check notice
Code scanning / CodeQL
Statement has no effect
Overview: What does this pull request change?
I made many changes to the functions defined in bezier.py to calculate Bézier curves faster. Here is a list of those changes, starting from the most important ones:
partial_quadratic_bezier_points
,split_quadratic_bezier
,subdivide_quadratic_bezier
andquadratic_bezier_remap
with generalized and optimized analogspartial_bezier_points
,split_bezier
,subdivide_bezier
andbezier_remap
bezier_remap
reused(OpenGL)VMobject.insert_n_curves_to_point_list
, so I rewrote the latter methods by usingbezier_remap
insteadget_smooth_cubic_bezier_handle_points
get_smooth_handle_points
toget_smooth_cubic_bezier_handle_points
get_smooth_cubic_bezier_handle_points_for_open_curve
andget_smooth_cubic_bezier_handle_points_for_closed_curve
diag_to_matrix
and thescipy
importis_close
, and slightly optimizedbezier
andinterpolate
Motivation and Explanation: Why and how do your changes improve the library?
partial_bezier_points
The original algorithm made a lot of calls to
bezier
, making multiple redundant intermediate calculations that could've been reused for the other Béziers. Instead, in the n-th degree case I calculate all of the necessary points in a single pass, improving from O(n³) to O(n²) time. Even more, the process can be encoded as a matrix multiplication, which is what I did for the simple cases (up to cubic curves), giving around 10x speedup.split_bezier and subdivide_bezier
Again, the processes can be encoded as matrix multiplications. In the case of
subdivide_bezier
, these matrices can even be memoized, as they're always the same.bezier_remap
I realized that I could essentially reuse the code for
VMobject.insert_n_curves_to_point_list
for this. Notice the use ofsubdivide_bezier
instead of repeated use ofpartial_bezier_points
, which is much faster than the original algorithm:Because of the above, I also straight up rewrote
(OpenGL)VMobject.insert_n_curves_to_point_list
to usebezier_remap
instead:get_smooth_handle_points
Now we arrive to the first function in
bezier.py
I modified, and the main reason I made this PR.I had a test scene with more than 100
ParametricFunction
s in it, which were all being updated. More than half of the render time was spent onParametricFunction.generate_points
, and one of the 3 main culprits wasVMobject.make_smooth
. There were many issues in that function I plan to address in another PR, but the one I'm going to address in this specific PR isget_smooth_handle_points
.The main bottleneck for this function is the call to
scipy.linalg.solve_banded
. Apparently, it spends more time validating the arrays passed as a parameter, than actually solving the system of equations.That's why I decided to manually implement the algorithm for solving these equations. But also, all the matrices follow the same pattern and their coefficients are pretty much always the same, varying only in size, so the diagonals can be hardcoded in the algorithm rather than stored explicitly. Some of them can even be memoized, which is what I did!
In the end, I got a speedup of around 3x. I can show a Snakeviz output of this, but that output includes a change to the
is_closed
function which really needed a rewrite, so lemme talk about it first.is_closed
Repeatedly using
np.allclose
to compare only two 3D points each time is actually very expensive and takes a painful part of the total time ofget_smooth_handle_points
. So I rewrote it to a simple "compare 1st coords, then compare 2nd coords, then compare 3rd coords", which resulted in almost a 30x speedup!SnakeViz output for
VMobject.make_smooth
before/after the changes toget_smooth_handle_points
andis_closed
:A closer look to
get_smooth_handle_points
:The red block at the right is the new
is_closed
. It now takes much less time to run! Combined with theget_smooth_handle_points
optimization, this latter function now has an overall speedup of around 4x.interpolate
Rewriting this function to use 1 product instead of 2, makes a speedup of around 30%:
bezier
First of all, I added the cases for Bézier curves of grade 0 and 1. This was originally intended for
partial_bezier_curves
, which repeatedly calledbezier
and was not prepared for these specific cases, falling back to the expensive general nth grade case:Then, I made a very slight optimization for the cubic Bëzier case:
It reported a small speedup of around 10% - 15%. Not too bad! I did something similar with the quadratic Bëzier:
Finally, the general nth grade Bézier case. I avoided the extensive use of the
choose
function which can be expensive, and instead used the recursive definition of Béziers I used for functions likepartial_bezier_points
andsubdivide_bezier
.Also,
bezier
now returns named functions instead of lambdas, which is good for debugging and profiling: now you can know which function is taking what part of the time when profiling your code.Links to added or changed documentation pages
manim.utils.bezier
https://manimce--3281.org.readthedocs.build/en/3281/reference/manim.utils.bezier.html
Reviewer Checklist