From fc379dab1855ae3739b8572a299c4cabaf5db4f7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 28 Jan 2023 10:10:51 -0800 Subject: [PATCH 01/21] Add a "clear" option for Mobjects This not only sets the submobject list to 0, but removes self from the parent lists --- manimlib/mobject/mobject.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index ebe82af6..f7b9f0f0 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -408,8 +408,13 @@ class Mobject(object): self.assemble_family() return self - def remove(self, *to_remove: Mobject, reassemble: bool = True): - for parent in self.get_family(): + def remove( + self, + *to_remove: Mobject, + reassemble: bool = True, + recurse: bool = True + ): + for parent in self.get_family(recurse): for child in to_remove: if child in parent.submobjects: parent.submobjects.remove(child) @@ -419,6 +424,9 @@ class Mobject(object): parent.assemble_family() return self + def clear(self): + self.remove(*self.submobjects, recurse=False) + def add_to_back(self, *mobjects: Mobject): self.set_submobjects(list_update(mobjects, self.submobjects)) return self From 8a18967ea4a13c2f4b27b22b7af7a99a01c91579 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 28 Jan 2023 10:11:10 -0800 Subject: [PATCH 02/21] Initial implementation of render groups in Scene --- manimlib/scene/scene.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 5c29d3e6..6ad771f4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -7,6 +7,7 @@ import platform import pyperclip import random import time +from functools import wraps from IPython.terminal import pt_inputhooks from IPython.terminal.embed import InteractiveShellEmbed @@ -37,6 +38,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.scene.scene_file_writer import SceneFileWriter from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.family_ops import recursive_mobject_remove +from manimlib.utils.iterables import batch_by_property from typing import TYPE_CHECKING @@ -110,6 +112,7 @@ class Scene(object): self.camera: Camera = Camera(**self.camera_config) self.file_writer = SceneFileWriter(self, **self.file_writer_config) self.mobjects: list[Mobject] = [self.camera.frame] + self.render_groups: list[Mobject] = [] self.id_to_mobject_map: dict[int, Mobject] = dict() self.num_plays: int = 0 self.time: float = 0 @@ -289,7 +292,7 @@ class Scene(object): def get_image(self) -> Image: if self.window is not None: self.camera.use_window_fbo(False) - self.camera.capture(*self.mobjects) + self.camera.capture(*self.render_groups) image = self.camera.get_image() if self.window is not None: self.camera.use_window_fbo(True) @@ -310,7 +313,7 @@ class Scene(object): if self.window: self.window.clear() - self.camera.capture(*self.mobjects) + self.camera.capture(*self.render_groups) if self.window: self.window.swap_buffers() @@ -369,6 +372,34 @@ class Scene(object): def get_mobject_family_members(self) -> list[Mobject]: return extract_mobject_family_members(self.mobjects) + def assemble_render_groups(self): + """ + Rendering is more efficient when VMobjects are grouped + together, so this function creates VGroups of all + clusters of adjacent VMobjects in the scene's mobject + list. + """ + for group in self.render_groups: + group.clear() + self.render_groups = [] + batches = batch_by_property( + self.mobjects, + lambda m: str(m.get_uniforms()) + str(m.apply_depth_test) + ) + self.render_groups = [ + batch[0].get_group_class()(*batch) + for batch, key in batches + ] + + def affects_mobject_list(func: Callable): + @wraps(func) + def wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + self.assemble_render_groups() + return self + return wrapper + + @affects_mobject_list def add(self, *new_mobjects: Mobject): """ Mobjects will be displayed, from background to @@ -395,6 +426,7 @@ class Scene(object): )) return self + @affects_mobject_list def replace(self, mobject: Mobject, *replacements: Mobject): if mobject in self.mobjects: index = self.mobjects.index(mobject) @@ -405,6 +437,7 @@ class Scene(object): ] return self + @affects_mobject_list def remove(self, *mobjects_to_remove: Mobject): """ Removes anything in mobjects from scenes mobject list, but in the event that one @@ -422,11 +455,13 @@ class Scene(object): self.add(*mobjects) return self + @affects_mobject_list def bring_to_back(self, *mobjects: Mobject): self.remove(*mobjects) self.mobjects = list(mobjects) + self.mobjects return self + @affects_mobject_list def clear(self): self.mobjects = [] return self From 9ef14c7260f3f14c4d1072080753ddd9b2735ca0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 11:38:54 -0800 Subject: [PATCH 03/21] Change default h_buff on Matrix --- example_scenes.py | 2 +- manimlib/mobject/matrix.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index 3134d484..9aa3e108 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -26,7 +26,7 @@ class OpeningManimExample(Scene): matrix = [[1, 1], [0, 1]] linear_transform_words = VGroup( Text("This is what the matrix"), - IntegerMatrix(matrix, include_background_rectangle=True, h_buff=1.0), + IntegerMatrix(matrix, include_background_rectangle=True), Text("looks like") ) linear_transform_words.arrange(RIGHT) diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index cbf5458a..001bfb02 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -76,7 +76,7 @@ class Matrix(VMobject): self, matrix: Sequence[Sequence[str | float | VMobject]], v_buff: float = 0.8, - h_buff: float = 1.3, + h_buff: float = 1.0, bracket_h_buff: float = 0.2, bracket_v_buff: float = 0.25, add_background_rectangles_to_entries: bool = False, From 979589a15688998b0ee6d1f95241e17a50345076 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 13:02:34 -0800 Subject: [PATCH 04/21] Change clicking behavior in InteractiveScene --- manimlib/scene/interactive_scene.py | 50 +++++++++++++++++------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 855eb1c4..51ce153e 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -29,6 +29,11 @@ from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.space_ops import get_norm from manimlib.utils.tex_file_writing import LatexError +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from manimlib.typing import Vect3 + SELECT_KEY = 's' UNSELECT_KEY = 'u' @@ -68,7 +73,7 @@ class InteractiveScene(Scene): """ corner_dot_config = dict( color=WHITE, - radius=0.025, + radius=0.05, glow_factor=2.0, ) selection_rectangle_stroke_color = WHITE @@ -273,7 +278,7 @@ class InteractiveScene(Scene): def get_corner_dots(self, mobject: Mobject) -> Mobject: dots = DotCloud(**self.corner_dot_config) - radius = self.corner_dot_config["radius"] + radius = float(self.corner_dot_config["radius"]) if mobject.get_depth() < 1e-2: vects = [DL, UL, UR, DR] else: @@ -389,7 +394,9 @@ class InteractiveScene(Scene): for mob in reversed(self.get_selection_search_set()): if self.selection_rectangle.is_touching(mob): additions.append(mob) - self.add_to_selection(*additions) + if self.selection_rectangle.get_arc_length() < 1e-2: + break + self.toggle_from_selection(*additions) def prepare_grab(self): mp = self.mouse_point.get_center() @@ -509,7 +516,6 @@ class InteractiveScene(Scene): super().on_key_release(symbol, modifiers) if chr(symbol) == SELECT_KEY: self.gather_new_selection() - # self.remove(self.crosshair) if chr(symbol) in GRAB_KEYS: self.is_grabbing = False elif chr(symbol) == INFORMATION_KEY: @@ -518,7 +524,7 @@ class InteractiveScene(Scene): self.prepare_resizing(about_corner=False) # Mouse actions - def handle_grabbing(self, point: np.ndarray): + def handle_grabbing(self, point: Vect3): diff = point - self.mouse_to_selection if self.window.is_key_pressed(ord(GRAB_KEY)): self.selection.move_to(diff) @@ -527,7 +533,7 @@ class InteractiveScene(Scene): elif self.window.is_key_pressed(ord(Y_GRAB_KEY)): self.selection.set_y(diff[1]) - def handle_resizing(self, point: np.ndarray): + def handle_resizing(self, point: Vect3): if not hasattr(self, "scale_about_point"): return vect = point - self.scale_about_point @@ -547,15 +553,16 @@ class InteractiveScene(Scene): about_point=self.scale_about_point ) - def handle_sweeping_selection(self, point: np.ndarray): + def handle_sweeping_selection(self, point: Vect3): mob = self.point_to_mobject( - point, search_set=self.get_selection_search_set(), + point, + search_set=self.get_selection_search_set(), buff=SMALL_BUFF ) if mob is not None: self.add_to_selection(mob) - def choose_color(self, point: np.ndarray): + def choose_color(self, point: Vect3): # Search through all mobject on the screen, not just the palette to_search = [ sm @@ -568,10 +575,9 @@ class InteractiveScene(Scene): self.selection.set_color(mob.get_color()) self.remove(self.color_palette) - def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: + def on_mouse_motion(self, point: Vect3, d_point: Vect3) -> None: super().on_mouse_motion(point, d_point) - ff_point = self.frame.to_fixed_frame_point(point) - self.crosshair.move_to(ff_point) + self.crosshair.move_to(self.frame.to_fixed_frame_point(point)) if self.is_grabbing: self.handle_grabbing(point) elif self.window.is_key_pressed(ord(RESIZE_KEY)): @@ -579,17 +585,19 @@ class InteractiveScene(Scene): elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL): self.handle_sweeping_selection(point) - def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None: + def on_mouse_drag( + self, + point: Vect3, + d_point: Vect3, + buttons: int, + modifiers: int + ) -> None: + super().on_mouse_drag(point, d_point, buttons, modifiers) + self.crosshair.move_to(self.frame.to_fixed_frame_point(point)) + + def on_mouse_release(self, point: Vect3, button: int, mods: int) -> None: super().on_mouse_release(point, button, mods) if self.color_palette in self.mobjects: self.choose_color(point) - return - mobject = self.point_to_mobject( - point, - search_set=self.get_selection_search_set(), - buff=1e-4, - ) - if mobject is not None: - self.toggle_from_selection(mobject) else: self.clear_selection() From 40bcb7e0f3f938218caa12f616b3382c37a71dba Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 13:02:56 -0800 Subject: [PATCH 05/21] Accept list of Vect3 as an input to Mobject.set_points --- manimlib/mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 4425ac36..09ad46b0 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -202,7 +202,7 @@ class Mobject(object): return self @affects_data - def set_points(self, points: Vect3Array) -> Self: + def set_points(self, points: Vect3Array | list[Vect3]) -> Self: self.resize_points(len(points), resize_func=resize_preserving_order) self.data["point"][:] = points return self From f293ccdff4c4e543d4ac994e626f387813516842 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 13:35:10 -0800 Subject: [PATCH 06/21] Add copy_frame_anim_call --- manimlib/scene/interactive_scene.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 51ce153e..3e296e00 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -8,8 +8,9 @@ from manimlib.animation.fading import FadeIn from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR -from manimlib.constants import FRAME_WIDTH, SMALL_BUFF +from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF from manimlib.constants import PI +from manimlib.constants import DEGREES from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle @@ -456,6 +457,7 @@ class InteractiveScene(Scene): else: self.save_mobject_to_file(self.selection) + # Key actions def on_key_press(self, symbol: int, modifiers: int) -> None: super().on_key_press(symbol, modifiers) char = chr(symbol) @@ -494,6 +496,8 @@ class InteractiveScene(Scene): self.toggle_selection_mode() elif char == "s" and modifiers == COMMAND_MODIFIER: self.save_selection_to_file() + elif char == PAN_3D_KEY and modifiers == COMMAND_MODIFIER: + self.copy_frame_anim_call() elif symbol in ARROW_SYMBOLS: self.nudge_selection( vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)], @@ -601,3 +605,18 @@ class InteractiveScene(Scene): self.choose_color(point) else: self.clear_selection() + + # Copying code to recreate state + def copy_frame_anim_call(self): + frame = self.frame + center = frame.get_center() + height = frame.get_height() + angles = frame.get_euler_angles() + + call = f"self.frame.animate.reorient" + call += str(tuple((angles / DEGREES).astype(int))) + if any(center != 0): + call += f".move_to({list(np.round(center, 2))})" + if height != FRAME_HEIGHT: + call += ".set_height({:.2f})".format(height) + pyperclip.copy(call) From f83c441210ec83c72b516dc493db1d7fb0816fb8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 13:35:46 -0800 Subject: [PATCH 07/21] Make it so that copying a mobject will copy its name, if applicable --- manimlib/scene/interactive_scene.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 3e296e00..3e2db303 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -3,6 +3,7 @@ from __future__ import annotations import itertools as it import numpy as np import pyperclip +from IPython.core.getipython import get_ipython from manimlib.animation.fading import FadeIn from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL @@ -26,6 +27,7 @@ from manimlib.mobject.types.vectorized_mobject import VHighlight from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.scene.scene import Scene from manimlib.scene.scene import SceneState +from manimlib.scene.scene import PAN_3D_KEY from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.space_ops import get_norm from manimlib.utils.tex_file_writing import LatexError @@ -234,9 +236,6 @@ class InteractiveScene(Scene): super().remove(*mobjects) self.regenerate_selection_search_set() - # def increment_time(self, dt: float) -> None: - # super().increment_time(dt) - # Related to selection def toggle_selection_mode(self): @@ -345,8 +344,17 @@ class InteractiveScene(Scene): # Functions for keyboard actions def copy_selection(self): - ids = map(id, self.selection) - pyperclip.copy(",".join(map(str, ids))) + names = [] + shell = get_ipython() + for mob in self.selection: + name = str(id(mob)) + if shell is None: + continue + for key, value in shell.user_ns.items(): + if mob is value: + name = key + names.append(name) + pyperclip.copy(", ".join(names)) def paste_selection(self): clipboard_str = pyperclip.paste() From 8820af65ec227eb318e99c01631222e386a2841b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 20:11:31 -0800 Subject: [PATCH 08/21] Check that scene has a camera frame in pixel_to_point_coords --- manimlib/window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/window.py b/manimlib/window.py index 7efbca96..9bc5090a 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -78,6 +78,9 @@ class Window(PygletWindow): py: int, relative: bool = False ) -> np.ndarray: + if not hasattr(self.scene, "frame"): + return np.zeros(3) + pixel_shape = np.array(self.size) fixed_frame_shape = np.array(FRAME_SHAPE) frame = self.scene.frame From f4a6f99b54f0e916c4f4816b29c21a2d93c8ba71 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 20:11:50 -0800 Subject: [PATCH 09/21] Check _use_winding_fill on the submobject level --- manimlib/mobject/types/vectorized_mobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 046dcc48..47dfeef5 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1297,11 +1297,11 @@ class VMobject(Mobject): back_stroke_datas.append(submob.data[stroke_names][indices]) if front_stroke: stroke_datas.append(submob.data[stroke_names][indices]) - if has_fill and self._use_winding_fill: + if has_fill and submob._use_winding_fill: data = submob.data[fill_names] data["base_point"][:] = data["point"][0] fill_datas.append(data[indices]) - if has_fill and not self._use_winding_fill: + if has_fill and not submob._use_winding_fill: fill_datas.append(submob.data[fill_names]) fill_indices.append(submob.get_triangulation()) if has_fill and not front_stroke: From ebf2ee58494b59a3895965aa627f9a7805aa686a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 20:12:06 -0800 Subject: [PATCH 10/21] Update tex patterns --- manimlib/utils/tex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py index 719d228a..791cdc5c 100644 --- a/manimlib/utils/tex.py +++ b/manimlib/utils/tex.py @@ -16,7 +16,7 @@ def num_tex_symbols(tex: str) -> int: # \begin{array}{cc}, etc. pattern = "|".join( rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?" - for s in ["begin", "end", "phantom"] + for s in ["begin", "end", "phantom", "text"] ) tex = re.sub(pattern, "", tex) From 8adf2a6e07e677b9e10f707eb3055729e386d6c7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 20:39:16 -0800 Subject: [PATCH 11/21] Partition render groups based on shader type, fixed_in_frame status, depth_test and whether the mobject is changing --- manimlib/scene/scene.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index f7c79173..b3089fa5 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -393,7 +393,12 @@ class Scene(object): self.render_groups = [] batches = batch_by_property( self.mobjects, - lambda m: str(m.get_uniforms()) + str(m.apply_depth_test) + lambda m: "".join([ + str(m.shader_dtype), + str(m.is_fixed_in_frame()), + str(m.depth_test), + str(m.is_changing()), + ]) ) self.render_groups = [ batch[0].get_group_class()(*batch) @@ -643,6 +648,7 @@ class Scene(object): else: self.update_mobjects(0) + @affects_mobject_list def play( self, *proto_animations: Animation | _AnimationBuilder, From eeadbe45425640627fb73e70b89be0c5f9c98b86 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 22:52:02 -0800 Subject: [PATCH 12/21] Small reshuffling --- manimlib/scene/scene.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index b3089fa5..51694c5d 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -388,18 +388,18 @@ class Scene(object): clusters of adjacent VMobjects in the scene's mobject list. """ - for group in self.render_groups: - group.clear() - self.render_groups = [] batches = batch_by_property( self.mobjects, - lambda m: "".join([ - str(m.shader_dtype), + lambda m: "|".join([ + str(m.shader_dtype.names), str(m.is_fixed_in_frame()), str(m.depth_test), str(m.is_changing()), ]) ) + + for group in self.render_groups: + group.clear() self.render_groups = [ batch[0].get_group_class()(*batch) for batch, key in batches From 47672d3b1e4b2bc5360e22f4354d1a60a1b4479a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 1 Feb 2023 22:52:59 -0800 Subject: [PATCH 13/21] Add checks for setting submobjects with existing list --- manimlib/mobject/mobject.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 09ad46b0..ee6c3eab 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -449,7 +449,9 @@ class Mobject(object): return self def set_submobjects(self, submobject_list: list[Mobject]) -> Self: - self.remove(*self.submobjects, reassemble=False) + if self.submobjects == submobject_list: + return self + self.clear() self.add(*submobject_list) return self From 9c03a40d682cbcd2767741a64fd47cefc0eb3118 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 10:39:30 -0800 Subject: [PATCH 14/21] Account for unnecessary calls to use_winding_fill --- manimlib/mobject/types/vectorized_mobject.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 47dfeef5..7b95030e 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -867,8 +867,10 @@ class VMobject(Mobject): # Alignment def align_points(self, vmobject: VMobject) -> Self: winding = self._use_winding_fill and vmobject._use_winding_fill - self.use_winding_fill(winding) - vmobject.use_winding_fill(winding) + if winding != self._use_winding_fill: + self.use_winding_fill(winding) + if winding != vmobject._use_winding_fill: + vmobject.use_winding_fill(winding) if self.get_num_points() == len(vmobject.get_points()): # If both have fill, and they have the same shape, just # give them the same triangulation so that it's not recalculated From b499caaa4599dec8cb62ecc09b2110dc290bb4d9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 10:40:09 -0800 Subject: [PATCH 15/21] Have SVG subdivide intersections if winding fill is not a default --- manimlib/mobject/svg/svg_mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index b9aea12d..0dcd0c67 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -309,6 +309,8 @@ class VMobjectFromSVGPath(VMobject): path_string = self.path_obj.d() if path_string not in PATH_TO_POINTS: self.handle_commands() + if not self._use_winding_fill: + self.subdivide_intersections() # Save for future use PATH_TO_POINTS[path_string] = self.get_points().copy() else: From 17cef427f135bfb284a10ec57ac25bd57e775765 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 10:42:24 -0800 Subject: [PATCH 16/21] Update fill shader alpha blending, and simplify the fill canvas --- manimlib/shader_wrapper.py | 12 +++---- .../shaders/quadratic_bezier_fill/frag.glsl | 2 +- manimlib/utils/shaders.py | 33 +++++-------------- 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 2c949057..dc5de477 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -287,24 +287,22 @@ class FillShaderWrapper(ShaderWrapper): return original_fbo = self.ctx.fbo - texture_fbo, texture_vao, null_rgb = self.fill_canvas + texture_fbo, texture_vao = self.fill_canvas - texture_fbo.clear(*null_rgb, 0.0) + texture_fbo.clear() texture_fbo.use() gl.glBlendFuncSeparate( # Ordinary blending for colors gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA, - # Just take the max of the alphas, given the shenanigans - # with how alphas are being used to compute winding numbers - gl.GL_ONE, gl.GL_ONE, + # The effect of blending with -a / (1 - a) + # should be to cancel out + gl.GL_ONE_MINUS_DST_ALPHA, gl.GL_ONE, ) - gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX) super().render() original_fbo.use() gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA) - gl.glBlendEquation(gl.GL_FUNC_ADD) texture_vao.render() diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index 199efbec..22d0edfb 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -33,7 +33,7 @@ void main() { cap is to make sure the original fragment color can be recovered even after blending with an (alpha = 1) color. */ - float a = 0.99 * frag_color.a; + float a = 0.95 * frag_color.a; if(winding && orientation < 0) a = -a / (1 - a); frag_color.a = a; diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 4f589d5b..2a24fd76 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -103,7 +103,7 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str: @lru_cache() -def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]: +def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray]: """ Because VMobjects with fill are rendered in a funny way, using alpha blending to effectively compute the winding number around @@ -123,26 +123,16 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu depth_texture = ctx.depth_texture(size=size) texture_fbo = ctx.framebuffer(texture, depth_texture) - # We'll paint onto a canvas with initially negative rgbs, and - # discard any pixels remaining close to this value. This is - # because alphas are effectively being used for another purpose, - # and we don't want to overlap with any colors one might actually - # use. It should be negative enough to be distinguishable from - # ordinary colors with some margin, but the farther it's pulled back - # from zero the more it will be true that overlapping filled objects - # with transparency have an unnaturally bright composition. - null_rgb = (-0.25, -0.25, -0.25) - simple_program = ctx.program( vertex_shader=''' #version 330 in vec2 texcoord; - out vec2 v_textcoord; + out vec2 uv; void main() { gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0); - v_textcoord = texcoord; + uv = texcoord; } ''', fragment_shader=''' @@ -150,31 +140,24 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu uniform sampler2D Texture; uniform sampler2D DepthTexture; - uniform vec3 null_rgb; - in vec2 v_textcoord; + in vec2 uv; out vec4 color; - const float MIN_DIST_TO_NULL = 0.2; - void main() { - color = texture(Texture, v_textcoord); + color = texture(Texture, uv); if(color.a == 0) discard; - if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard; - // Un-blend from the null value - color.rgb -= (1 - color.a) * null_rgb; // Counteract scaling in fill frag - color.a *= 1.01; + color.a *= 1.06; - gl_FragDepth = texture(DepthTexture, v_textcoord)[0]; + gl_FragDepth = texture(DepthTexture, uv)[0]; } ''', ) simple_program['Texture'].value = get_texture_id(texture) simple_program['DepthTexture'].value = get_texture_id(depth_texture) - simple_program['null_rgb'].value = null_rgb verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) fill_texture_vao = ctx.simple_vertex_array( @@ -183,4 +166,4 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu 'texcoord', mode=moderngl.TRIANGLE_STRIP ) - return (texture_fbo, fill_texture_vao, null_rgb) + return (texture_fbo, fill_texture_vao) From acb4b1c6b3e12422c94ffc31a1fb61594543e9ac Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 12:04:23 -0800 Subject: [PATCH 17/21] Finalize color at the vertex level, rather than the fragment level, for fill --- manimlib/shaders/quadratic_bezier_fill/frag.glsl | 14 ++++++-------- manimlib/shaders/quadratic_bezier_fill/geom.glsl | 10 ++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index 22d0edfb..4cfed975 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -6,16 +6,12 @@ in vec4 color; in float fill_all; in float orientation; in vec2 uv_coords; -in vec3 point; -in vec3 unit_normal; out vec4 frag_color; -#INSERT finalize_color.glsl - void main() { if (color.a == 0) discard; - frag_color = finalize_color(color, point, unit_normal); + frag_color = color; /* We want negatively oriented triangles to be canceled with positively oriented ones. The easiest way to do this is to give them negative alpha, @@ -33,9 +29,11 @@ void main() { cap is to make sure the original fragment color can be recovered even after blending with an (alpha = 1) color. */ - float a = 0.95 * frag_color.a; - if(winding && orientation < 0) a = -a / (1 - a); - frag_color.a = a; + if(winding){ + float a = 0.95 * frag_color.a; + if(orientation < 0) a = -a / (1 - a); + frag_color.a = a; + } if (bool(fill_all)) return; diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index c9428e67..99e10049 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -14,8 +14,6 @@ in vec3 v_unit_normal[3]; out vec4 color; out float fill_all; out float orientation; -out vec3 point; -out vec3 unit_normal; // uv space is where the curve coincides with y = x^2 out vec2 uv_coords; @@ -28,9 +26,12 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3]( // Analog of import for manim only #INSERT emit_gl_Position.glsl +#INSERT finalize_color.glsl void emit_triangle(vec3 points[3], vec4 v_color[3]){ + vec3 unit_normal = v_unit_normal[1]; + orientation = sign(determinant(mat3( unit_normal, points[1] - points[0], @@ -39,8 +40,7 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){ for(int i = 0; i < 3; i++){ uv_coords = SIMPLE_QUADRATIC[i]; - color = v_color[i]; - point = points[i]; + color = finalize_color(v_color[i], points[i], unit_normal); emit_gl_Position(points[i]); EmitVertex(); } @@ -61,8 +61,6 @@ void main(){ // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - unit_normal = v_unit_normal[1]; - if(winding){ // Emit main triangle fill_all = 1.0; From e5eed7c36a23c0e23f5c4a44ab859a1c54d0c256 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 13:28:06 -0800 Subject: [PATCH 18/21] Batch render groups by Mobject type --- manimlib/scene/scene.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 51694c5d..0a764737 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -383,20 +383,11 @@ class Scene(object): def assemble_render_groups(self): """ - Rendering is more efficient when VMobjects are grouped - together, so this function creates VGroups of all - clusters of adjacent VMobjects in the scene's mobject - list. + Rendering can be more efficient when mobjects of the + same type are grouped together, so this function creates + Groups of all clusters of adjacent Mobjects in the scene """ - batches = batch_by_property( - self.mobjects, - lambda m: "|".join([ - str(m.shader_dtype.names), - str(m.is_fixed_in_frame()), - str(m.depth_test), - str(m.is_changing()), - ]) - ) + batches = batch_by_property(self.mobjects, lambda m: str(type(m))) for group in self.render_groups: group.clear() From b9d6dcd67d5c59b2f6a9ee7e82a8d0f2e9f87a00 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 14:26:08 -0800 Subject: [PATCH 19/21] Save _has_fill and _has_stroke to prevent unnecessary recalculation --- manimlib/mobject/types/vectorized_mobject.py | 37 +++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index be5d284d..a9d2abf3 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -111,6 +111,8 @@ class VMobject(Mobject): self.anti_alias_width = anti_alias_width self.fill_border_width = fill_border_width self._use_winding_fill = use_winding_fill + self._has_fill = False + self._has_stroke = False self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype='i4') @@ -134,6 +136,16 @@ class VMobject(Mobject): return super().add(*vmobjects) # Colors + def note_changed_fill(self) -> Self: + for submob in self.get_family(): + submob._has_fill = submob.has_fill() + return self + + def note_changed_stroke(self) -> Self: + for submob in self.get_family(): + submob._has_stroke = submob.has_stroke() + return self + def init_colors(self): self.set_fill( color=self.fill_color, @@ -164,6 +176,10 @@ class VMobject(Mobject): for name in names: super().set_rgba_array(rgba_array, name, recurse) + if name == "fill_rgba": + self.note_changed_fill() + elif name == "stroke_rgba": + self.note_changed_stroke() return self def set_fill( @@ -177,6 +193,7 @@ class VMobject(Mobject): if border_width is not None: for mob in self.get_family(recurse): mob.data["fill_border_width"] = border_width + self.note_changed_fill() return self def set_stroke( @@ -202,6 +219,8 @@ class VMobject(Mobject): if background is not None: for mob in self.get_family(recurse): mob.stroke_behind = background + + self.note_changed_stroke() return self def set_backstroke( @@ -255,6 +274,8 @@ class VMobject(Mobject): if shading is not None: mob.set_shading(*shading, recurse=False) + self.note_changed_fill() + self.note_changed_stroke() return self def get_style(self) -> dict[str, Any]: @@ -378,10 +399,12 @@ class VMobject(Mobject): return self.uniforms["anti_alias_width"] def has_stroke(self) -> bool: - return any(self.data['stroke_width']) and any(self.data['stroke_rgba'][:, 3]) + data = self.data if len(self.data) > 0 else self._data_defaults + return any(data['stroke_width']) and any(data['stroke_rgba'][:, 3]) def has_fill(self) -> bool: - return any(self.data['fill_rgba'][:, 3]) + data = self.data if len(self.data) > 0 else self._data_defaults + return any(data['fill_rgba'][:, 3]) def get_opacity(self) -> float: if self.has_fill(): @@ -971,6 +994,10 @@ class VMobject(Mobject): *args, **kwargs ) -> Self: super().interpolate(mobject1, mobject2, alpha, *args, **kwargs) + + self._has_stroke = mobject1._has_stroke or mobject2._has_stroke + self._has_fill = mobject1._has_fill or mobject2._has_fill + if self.has_fill() and not self._use_winding_fill: tri1 = mobject1.get_triangulation() tri2 = mobject2.get_triangulation() @@ -1202,6 +1229,8 @@ class VMobject(Mobject): @triggers_refreshed_triangulation def set_data(self, data: np.ndarray) -> Self: super().set_data(data) + self.note_changed_fill() + self.note_changed_stroke() return self # TODO, how to be smart about tangents here? @@ -1291,8 +1320,8 @@ class VMobject(Mobject): for submob in family: submob.get_joint_products() indices = submob.get_outer_vert_indices() - has_fill = submob.has_fill() - has_stroke = submob.has_stroke() + has_fill = submob._has_fill + has_stroke = submob._has_stroke back_stroke = has_stroke and submob.stroke_behind front_stroke = has_stroke and not submob.stroke_behind if back_stroke: From bd89056c8e78297645bc253e4ff3a87dce3da133 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 14:29:37 -0800 Subject: [PATCH 20/21] Only recalculate outer_vert_indices when points are resized --- manimlib/mobject/types/vectorized_mobject.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a9d2abf3..f54d18ec 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1053,13 +1053,22 @@ class VMobject(Mobject): vmob.pointwise_become_partial(self, a, b) return vmob + def resize_points( + self, + new_length: int, + resize_func: Callable[[np.ndarray, int], np.ndarray] = resize_array + ) -> Self: + super().resize_points(new_length, resize_func) + + n_curves = self.get_num_curves() + # Creates the pattern (0, 1, 2, 2, 3, 4, 4, 5, 6, ...) + self.outer_vert_indices = (np.arange(1, 3 * n_curves + 1) * 2) // 3 + return self + def get_outer_vert_indices(self) -> np.ndarray: """ Returns the pattern (0, 1, 2, 2, 3, 4, 4, 5, 6, ...) """ - n_curves = self.get_num_curves() - if len(self.outer_vert_indices) != 3 * n_curves: - self.outer_vert_indices = (np.arange(1, 3 * n_curves + 1) * 2) // 3 return self.outer_vert_indices # Data for shaders that may need refreshing From 63dbe3b23fa1c7ed0aab85804caf3d3214a1287d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 14:32:55 -0800 Subject: [PATCH 21/21] More direct check for family_members_with_points --- manimlib/mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 87b2d801..1f794aff 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -376,7 +376,7 @@ class Mobject(object): return [self] def family_members_with_points(self) -> list[Self]: - return [m for m in self.get_family() if m.has_points()] + return [m for m in self.family if len(m.data) > 0] def get_ancestors(self, extended: bool = False) -> list[Mobject]: """