Skip to content

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e445f84
Optimized get_smooth_handle_points and is_closed in bezier.py
chopan050 Jul 8, 2023
2b393b4
Added case n = 0 and 1 in bezier.py's bezier to optimize partial_bezi…
chopan050 Jul 10, 2023
03d7c8b
Drastic improvement of partial_bezier_points
chopan050 Jul 10, 2023
d6aa89a
Added split_bezier and subdivide_bezier
chopan050 Jul 11, 2023
ba3a3df
Removed rtol from is_closed
chopan050 Jul 11, 2023
0c1f5d9
Merge branch 'main' of https://github.com/ManimCommunity/manim into o…
chopan050 Jul 11, 2023
3c97685
Reverted partial_quadratic_bezier_points' output to Python list
chopan050 Jul 11, 2023
0056a49
Fixed wrong variable name in split_bezier
chopan050 Jul 11, 2023
de0d9c7
Imported choose function again. Whoops
chopan050 Jul 11, 2023
08765de
Fixed checking if b was different from 0 (should have been 1) in part…
chopan050 Jul 11, 2023
febb7fa
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Jul 13, 2023
eea3915
Changed placement of comments and global statements
chopan050 Jul 16, 2023
580e3cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 16, 2023
c2acd34
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Jul 16, 2023
40ffda3
Merge branch 'ManimCommunity:main' into optimized_partial_smooth_beziers
chopan050 Aug 8, 2023
74f6200
Merge branch 'main' of https://github.com/ManimCommunity/manim into o…
chopan050 Aug 18, 2023
fdb5390
More Bezier optimizations
chopan050 Nov 3, 2023
26a430f
Added bezier_remap to __all__
chopan050 Nov 3, 2023
17ae3b7
Removed unused local variable at split_bezier
chopan050 Nov 3, 2023
cd8abfb
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Nov 3, 2023
c1c5eae
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Nov 29, 2023
2846729
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 29, 2023
e5da3f0
Rewrite get_quadratic... for readability
chopan050 Nov 29, 2023
12589df
Correctly typed bezier function and fixed zero_bezier return
chopan050 Nov 30, 2023
877ba37
Removed partial_quadratic_bezier_points and rewrote OpenGLVMobject.su…
chopan050 Dec 1, 2023
847d0f4
Removed original functions for quadratic Beziers only, replaced them …
chopan050 Dec 1, 2023
2701473
Removed diag_to_matrix
chopan050 Dec 1, 2023
d046fd7
Removed scipy import from bezier.py
chopan050 Dec 1, 2023
80101b4
Updating docs, part 1
chopan050 Dec 1, 2023
09f7e8b
Updating docs, part 2
chopan050 Dec 1, 2023
ee00486
Renamed get_smooth_handle_points to get_handles_for_smooth_spline
chopan050 Dec 1, 2023
9e27f12
Minor fixes
chopan050 Dec 1, 2023
a576280
Updating docs, part 3
chopan050 Dec 1, 2023
3f43bf1
Updating docs, part 4
chopan050 Dec 2, 2023
e3cb964
Updating docs, part 5
chopan050 Dec 2, 2023
d68234e
Rewrote (OpenGL)VMobject.insert_n_curves_to_point_list using bezier_r…
chopan050 Dec 2, 2023
a37043c
Rewrote (OpenGL)VMobject.insert_n_curves_to_point_list using bezier_r…
chopan050 Dec 2, 2023
9407e14
Updating docs, part 5 - Fixed nth_degree_bezier's IO
chopan050 Dec 2, 2023
f572ea7
Final Bezier doc fixes
chopan050 Dec 2, 2023
123cbdd
Fixed typo in get_smooth... which ruined all the Beziers
chopan050 Dec 2, 2023
afd2cce
Fixed typos in docstrings
chopan050 Dec 2, 2023
bb677a7
Update manim/utils/bezier.py fix typo in math equation
MrDiver Dec 2, 2023
2bbfb58
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Dec 3, 2023
c47ad6c
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Dec 29, 2023
ae59f1c
Reverted rename of 'get_smooth_cubic_bezier_handle_points'
chopan050 Dec 29, 2023
d579eea
Fix subdivision matrix building and add test for it
chopan050 Dec 29, 2023
d160ea4
Removed prints
chopan050 Dec 29, 2023
a849992
Merge branch 'main' into optimized_partial_smooth_beziers
chopan050 Dec 29, 2023
05fe1f0
Fix n-th case Bézier subdivision and added test for it
chopan050 Dec 30, 2023
e6025e6
Add tests for partial_bezier_points and split_bezier
chopan050 Dec 30, 2023
38cef5c
Merge branch 'main' of https://github.com/ManimCommunity/manim into o…
chopan050 May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 34 additions & 60 deletions manim/mobject/opengl/opengl_vectorized_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
from manim.renderer.shader_wrapper import ShaderWrapper
from manim.utils.bezier import (
bezier,
bezier_remap,
get_quadratic_approximation_of_cubic,
get_smooth_cubic_bezier_handle_points,
integer_interpolate,
interpolate,
partial_quadratic_bezier_points,
partial_bezier_points,
proportions_along_bezier_curve_for_point,
quadratic_bezier_remap,
subdivide_bezier,
)
from manim.utils.color import BLACK, WHITE, ManimColor, ParsableManimColor
from manim.utils.config_ops import _Data
Expand Down Expand Up @@ -547,21 +548,23 @@ def is_closed(self):
def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True):
vmobs = [vm for vm in self.get_family(recurse) if vm.has_points()]
for vmob in vmobs:
new_points = []
for tup in vmob.get_bezier_tuples():
angle = angle_between_vectors(tup[1] - tup[0], tup[2] - tup[1])
if angle > angle_threshold:
n = int(np.ceil(angle / angle_threshold))
alphas = np.linspace(0, 1, n + 1)
new_points.extend(
[
partial_quadratic_bezier_points(tup, a1, a2)
for a1, a2 in zip(alphas, alphas[1:])
],
)
else:
new_points.append(tup)
vmob.set_points(np.vstack(new_points))
beziers = vmob.get_bezier_tuples()
angles = np.empty(beziers.shape[0])
for i, bez in enumerate(beziers):
angles[i] = angle_between_vectors(bez[1] - bez[0], bez[2] - bez[1])

num_parts_per_bezier = np.ceil(angles / angle_threshold).astype(int)
acc_parts = np.add.accumulate(num_parts_per_bezier)
end_indices = self.n_points_per_curve * acc_parts
num_final_points = end_indices[-1]
new_points = np.empty((num_final_points, vmob.points.shape[1]))

start = 0
for bez, end, n in zip(beziers, end_indices, num_parts_per_bezier):
new_points[start:end] = subdivide_bezier(bez, n)
start = end

vmob.set_points(new_points)
return self

def add_points_as_corners(self, points):
Expand Down Expand Up @@ -1255,53 +1258,30 @@ def insert_n_curves(self, n: int, recurse=True) -> OpenGLVMobject:
return self

def insert_n_curves_to_point_list(self, n: int, points: np.ndarray) -> np.ndarray:
"""Given an array of k points defining a bezier curves
(anchors and handles), returns points defining exactly
k + n bezier curves.
"""Given an array of ``points`` defining :math:`k` Bézier curves (anchors and handles),
returns an array of points defining exactly :math:`k + n` Bézier curves.

Parameters
----------
n
Number of desired curves.
Number of desired curves to add.
points
Starting points.

Returns
-------
np.ndarray
:class:`np.ndarray`
Points generated.
"""
nppc = self.n_points_per_curve
if len(points) == 1:
nppc = self.n_points_per_curve
return np.repeat(points, nppc * n, 0)

bezier_groups = self.get_bezier_tuples_from_points(points)
norms = np.array([np.linalg.norm(bg[nppc - 1] - bg[0]) for bg in bezier_groups])
total_norm = sum(norms)
# Calculate insertions per curve (ipc)
if total_norm < 1e-6:
ipc = [n] + [0] * (len(bezier_groups) - 1)
else:
ipc = np.round(n * norms / sum(norms)).astype(int)

diff = n - sum(ipc)
for _ in range(diff):
ipc[np.argmin(ipc)] += 1
for _ in range(-diff):
ipc[np.argmax(ipc)] -= 1

new_length = sum(x + 1 for x in ipc)
new_points = np.empty((new_length, nppc, 3))
i = 0
for group, n_inserts in zip(bezier_groups, ipc):
# What was once a single quadratic curve defined
# by "group" will now be broken into n_inserts + 1
# smaller quadratic curves
alphas = np.linspace(0, 1, n_inserts + 2)
for a1, a2 in zip(alphas, alphas[1:]):
new_points[i] = partial_quadratic_bezier_points(group, a1, a2)
i = i + 1
return np.vstack(new_points)
bezier_tuples = self.get_bezier_tuples_from_points(points)
new_number_of_curves = bezier_tuples.shape[0] + n
new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves)
new_points = new_bezier_tuples.reshape(-1, 3)
return new_points

def interpolate(self, mobject1, mobject2, alpha, *args, **kwargs):
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
Expand Down Expand Up @@ -1354,32 +1334,26 @@ def pointwise_become_partial(
return self
if lower_index == upper_index:
self.append_points(
partial_quadratic_bezier_points(
partial_bezier_points(
bezier_triplets[lower_index],
lower_residue,
upper_residue,
),
)
else:
self.append_points(
partial_quadratic_bezier_points(
bezier_triplets[lower_index], lower_residue, 1
),
partial_bezier_points(bezier_triplets[lower_index], lower_residue, 1),
)
inner_points = bezier_triplets[lower_index + 1 : upper_index]
if len(inner_points) > 0:
if remap:
new_triplets = quadratic_bezier_remap(
inner_points, num_quadratics - 2
)
new_triplets = bezier_remap(inner_points, num_quadratics - 2)
else:
new_triplets = bezier_triplets

self.append_points(np.asarray(new_triplets).reshape(-1, 3))
self.append_points(
partial_quadratic_bezier_points(
bezier_triplets[upper_index], 0, upper_residue
),
partial_bezier_points(bezier_triplets[upper_index], 0, upper_residue),
)
return self

Expand Down
53 changes: 13 additions & 40 deletions manim/mobject/types/vectorized_mobject.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
)
from manim.utils.bezier import (
bezier,
get_smooth_handle_points,
bezier_remap,
get_smooth_cubic_bezier_handle_points,
integer_interpolate,
interpolate,
partial_bezier_points,
Expand Down Expand Up @@ -1011,7 +1012,7 @@ def change_anchor_mode(self, mode: Literal["jagged", "smooth"]) -> Self:
# The append is needed as the last element is not reached when slicing with numpy.
anchors = np.append(subpath[::nppcc], subpath[-1:], 0)
if mode == "smooth":
h1, h2 = get_smooth_handle_points(anchors)
h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
else: # mode == "jagged"
# The following will make the handles aligned with the anchors, thus making the bezier curve a segment
a1 = anchors[:-1]
Expand Down Expand Up @@ -1675,58 +1676,30 @@ def insert_n_curves(self, n: int) -> Self:

def insert_n_curves_to_point_list(
self, n: int, points: Point3D_Array
) -> npt.NDArray[BezierPoints]:
"""Given an array of k points defining a bezier curves (anchors and handles), returns points defining exactly k + n bezier curves.
) -> Point3D_Array:
"""Given an array of ``points`` defining :math:`k` Bézier curves (anchors and handles),
returns an array of points defining exactly :math:`k + n` Bézier curves.

Parameters
----------
n
Number of desired curves.
Number of desired curves to add.
points
Starting points.

Returns
-------
:class:`Point3D_Array`
Points generated.
"""

if len(points) == 1:
nppcc = self.n_points_per_cubic_curve
return np.repeat(points, nppcc * n, 0)
bezier_quads = self.get_cubic_bezier_tuples_from_points(points)
curr_num = len(bezier_quads)
target_num = curr_num + n
# This is an array with values ranging from 0
# up to curr_num, with repeats such that
# it's total length is target_num. For example,
# with curr_num = 10, target_num = 15, this would
# be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
repeat_indices = (np.arange(target_num, dtype="i") * curr_num) // target_num

# If the nth term of this list is k, it means
# that the nth curve of our path should be split
# into k pieces.
# In the above example our array had the following elements
# [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9]
# We have two 0s, one 1, two 2s and so on.
# The split factors array would hence be:
# [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
split_factors = np.zeros(curr_num, dtype="i")
for val in repeat_indices:
split_factors[val] += 1

new_points = np.zeros((0, self.dim))
for quad, sf in zip(bezier_quads, split_factors):
# What was once a single cubic curve defined
# by "quad" will now be broken into sf
# smaller cubic curves
alphas = np.linspace(0, 1, sf + 1)
for a1, a2 in zip(alphas, alphas[1:]):
new_points = np.append(
new_points,
partial_bezier_points(quad, a1, a2),
axis=0,
)

bezier_tuples = self.get_cubic_bezier_tuples_from_points(points)
new_number_of_curves = bezier_tuples.shape[0] + n
new_bezier_tuples = bezier_remap(bezier_tuples, new_number_of_curves)
new_points = new_bezier_tuples.reshape(-1, 3)
return new_points

def align_rgbas(self, vmobject: VMobject) -> Self:
Expand Down
Loading