From 8eac976c8dfe72d7f47bf9b1143b9d209c0793aa Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 6 Sep 2024 13:35:32 -0500 Subject: [PATCH 01/12] Tweak moderngl import in ImageMobject --- manimlib/mobject/types/image_mobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index c35ef267..40d94567 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -1,8 +1,8 @@ from __future__ import annotations import numpy as np +import moderngl from PIL import Image -from moderngl import TRIANGLES from manimlib.constants import DL, DR, UL, UR from manimlib.mobject.mobject import Mobject @@ -25,7 +25,7 @@ class ImageMobject(Mobject): ('im_coords', np.float32, (2,)), ('opacity', np.float32, (1,)), ] - render_primitive: int = TRIANGLES + render_primitive: int = moderngl.TRIANGLES def __init__( self, From 95fca885c920e469ca718e72d383f8790e3a6dca Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Sep 2024 17:20:19 -0500 Subject: [PATCH 02/12] Push clip plane functionality up to all Mobjects --- manimlib/mobject/mobject.py | 16 ++++++++++++++++ manimlib/mobject/types/surface.py | 19 ------------------- .../shaders/inserts/emit_gl_Position.glsl | 5 +++++ manimlib/shaders/surface/vert.glsl | 6 ------ 4 files changed, 21 insertions(+), 25 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index a09d6116..eca58349 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -142,6 +142,7 @@ class Mobject(object): self.uniforms: UniformDict = { "is_fixed_in_frame": 0.0, "shading": np.array(self.shading, dtype=float), + "clip_plane": np.zeros(4), } def init_colors(self): @@ -1946,6 +1947,21 @@ class Mobject(object): mob.depth_test = False return self + def set_clip_plane( + self, + vect: Vect3 | None = None, + threshold: float | None = None + ) -> Self: + if vect is not None: + self.uniforms["clip_plane"][:3] = vect + if threshold is not None: + self.uniforms["clip_plane"][3] = threshold + return self + + def deactivate_clip_plane(self) -> Self: + self.uniforms["clip_plane"][:] = 0 + return self + # Shader code manipulation @affects_data diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index 82ac9acc..1a0eabb4 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -65,10 +65,6 @@ class Surface(Mobject): ) self.compute_triangle_indices() - def init_uniforms(self): - super().init_uniforms() - self.uniforms["clip_plane"] = np.zeros(4) - def uv_func(self, u: float, v: float) -> tuple[float, float, float]: # To be implemented in subclasses return (u, v, 0.0) @@ -216,21 +212,6 @@ class Surface(Mobject): self.add_updater(updater) return self - def set_clip_plane( - self, - vect: Vect3 | None = None, - threshold: float | None = None - ) -> Self: - if vect is not None: - self.uniforms["clip_plane"][:3] = vect - if threshold is not None: - self.uniforms["clip_plane"][3] = threshold - return self - - def deactivate_clip_plane(self) -> Self: - self.uniforms["clip_plane"][:] = 0 - return self - def get_shader_vert_indices(self) -> np.ndarray: return self.get_triangle_indices() diff --git a/manimlib/shaders/inserts/emit_gl_Position.glsl b/manimlib/shaders/inserts/emit_gl_Position.glsl index 3e270b96..2882b422 100644 --- a/manimlib/shaders/inserts/emit_gl_Position.glsl +++ b/manimlib/shaders/inserts/emit_gl_Position.glsl @@ -2,6 +2,7 @@ uniform float is_fixed_in_frame; uniform mat4 view; uniform float focal_distance; uniform vec3 frame_rescale_factors; +uniform vec4 clip_plane; void emit_gl_Position(vec3 point){ vec4 result = vec4(point, 1.0); @@ -13,4 +14,8 @@ void emit_gl_Position(vec3 point){ // Flip and scale to prevent premature clipping result.z *= -0.1; gl_Position = result; + + if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){ + gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane); + } } diff --git a/manimlib/shaders/surface/vert.glsl b/manimlib/shaders/surface/vert.glsl index f79586d3..96b85bf4 100644 --- a/manimlib/shaders/surface/vert.glsl +++ b/manimlib/shaders/surface/vert.glsl @@ -1,7 +1,5 @@ #version 330 -uniform vec4 clip_plane; - in vec3 point; in vec3 du_point; in vec3 dv_point; @@ -17,8 +15,4 @@ void main(){ emit_gl_Position(point); vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point)); v_color = finalize_color(rgba, point, normalize(normal)); - - if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){ - gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane); - } } \ No newline at end of file From 3e7244b90bd0d8ecee51d457f41678d8c40544f8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 21 Sep 2024 12:15:06 -0400 Subject: [PATCH 03/12] Fix bad argument --- manimlib/utils/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/debug.py b/manimlib/utils/debug.py index daa66165..f3e5ce99 100644 --- a/manimlib/utils/debug.py +++ b/manimlib/utils/debug.py @@ -27,6 +27,6 @@ def index_labels( label = Integer(n) label.set_height(label_height) label.move_to(submob) - label.set_stroke(BLACK, 5, background=True) + label.set_backstroke(BLACK, 5) labels.add(label) return labels From 62a4ea5165617bf8f0472b1d5f1cbaf504d0a943 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 21 Sep 2024 12:15:29 -0400 Subject: [PATCH 04/12] Update description of remove_list_redundancies --- manimlib/utils/iterables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/iterables.py b/manimlib/utils/iterables.py index e213cd91..854067cb 100644 --- a/manimlib/utils/iterables.py +++ b/manimlib/utils/iterables.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: def remove_list_redundancies(lst: Sequence[T]) -> list[T]: """ - Used instead of list(set(l)) to maintain order + Remove duplicate elements while preserving order. Keeps the last occurrence of each element """ return list(reversed(dict.fromkeys(reversed(lst)))) From 080410930158588e380a8d8b3f462d9001afad89 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 21 Sep 2024 12:15:37 -0400 Subject: [PATCH 05/12] Flatten uniform arrays --- manimlib/utils/shaders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index bb950119..17f2ea7f 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -67,7 +67,7 @@ def set_program_uniform( uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid] if type(value) is np.ndarray and value.ndim > 0: - value = tuple(value) + value = tuple(value.flatten()) if uniform_mirror.get(name, None) == value: return False From 217eb6b486cf0d1c8f6e0d9d6fb06593165bab3c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 21 Sep 2024 12:16:09 -0400 Subject: [PATCH 06/12] Move new VectorField from optics projects into main repo --- manimlib/mobject/vector_field.py | 183 ++++++++++++++++++++++++++++++- 1 file changed, 181 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 38ca9dc5..df784190 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -5,7 +5,8 @@ import itertools as it import numpy as np from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH -from manimlib.constants import WHITE +from manimlib.constants import BLUE, WHITE +from manimlib.constants import ORIGIN from manimlib.animation.indication import VShowPassingFlash from manimlib.mobject.geometry import Arrow from manimlib.mobject.types.vectorized_mobject import VGroup @@ -15,6 +16,7 @@ from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.color import get_colormap_list from manimlib.utils.color import rgb_to_color from manimlib.utils.dict_ops import merge_dicts_recursively +from manimlib.utils.iterables import cartesian_product from manimlib.utils.rate_functions import linear from manimlib.utils.simple_functions import sigmoid from manimlib.utils.space_ops import get_norm @@ -118,7 +120,184 @@ def get_sample_points_from_coordinate_system( # Mobjects -class VectorField(VGroup): + +class VectorField(VMobject): + def __init__( + self, + func, + stroke_color: ManimColor = BLUE, + stroke_opacity: float = 1.0, + center: Vect3 = ORIGIN, + sample_points: Optional[Vect3Array] = None, + x_density: float = 2.0, + y_density: float = 2.0, + z_density: float = 2.0, + width: float = 14.0, + height: float = 8.0, + depth: float = 0.0, + stroke_width: float = 2, + tip_width_ratio: float = 4, + tip_len_to_width: float = 0.01, + max_vect_len: float | None = None, + min_drawn_norm: float = 1e-2, + flat_stroke: bool = False, + norm_to_opacity_func=None, + norm_to_rgb_func=None, + **kwargs + ): + self.func = func + self.stroke_width = stroke_width + self.tip_width_ratio = tip_width_ratio + self.tip_len_to_width = tip_len_to_width + self.min_drawn_norm = min_drawn_norm + self.norm_to_opacity_func = norm_to_opacity_func + self.norm_to_rgb_func = norm_to_rgb_func + + if max_vect_len is not None: + self.max_vect_len = max_vect_len + else: + densities = np.array([x_density, y_density, z_density]) + dims = np.array([width, height, depth]) + self.max_vect_len = 1.0 / densities[dims > 0].mean() + + if sample_points is None: + self.sample_points = self.get_sample_points( + center, width, height, depth, + x_density, y_density, z_density + ) + else: + self.sample_points = sample_points + + self.init_base_stroke_width_array(len(self.sample_points)) + + super().__init__( + stroke_color=stroke_color, + stroke_opacity=stroke_opacity, + flat_stroke=flat_stroke, + **kwargs + ) + + n_samples = len(self.sample_points) + self.set_points(np.zeros((8 * n_samples - 1, 3))) + self.set_stroke(width=stroke_width) + self.set_joint_type('no_joint') + self.update_vectors() + + def get_sample_points( + self, + center: np.ndarray, + width: float, + height: float, + depth: float, + x_density: float, + y_density: float, + z_density: float + ) -> np.ndarray: + to_corner = np.array([width / 2, height / 2, depth / 2]) + spacings = 1.0 / np.array([x_density, y_density, z_density]) + to_corner = spacings * (to_corner / spacings).astype(int) + lower_corner = center - to_corner + upper_corner = center + to_corner + spacings + return cartesian_product(*( + np.arange(low, high, space) + for low, high, space in zip(lower_corner, upper_corner, spacings) + )) + + def init_base_stroke_width_array(self, n_sample_points): + arr = np.ones(8 * n_sample_points - 1) + arr[4::8] = self.tip_width_ratio + arr[5::8] = self.tip_width_ratio * 0.5 + arr[6::8] = 0 + arr[7::8] = 0 + self.base_stroke_width_array = arr + + def set_sample_points(self, sample_points: Vect3Array): + self.sample_points = sample_points + return self + + def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True): + super().set_stroke(color, None, opacity, behind, flat, recurse) + if width is not None: + self.set_stroke_width(float(width)) + return self + + def set_stroke_width(self, width: float): + if self.get_num_points() > 0: + self.get_stroke_widths()[:] = width * self.base_stroke_width_array + self.stroke_width = width + return self + + def update_vectors(self): + tip_width = self.tip_width_ratio * self.stroke_width + tip_len = self.tip_len_to_width * tip_width + samples = self.sample_points + + # Get raw outputs and lengths + outputs = self.func(samples) + norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis] + + # How long should the arrows be drawn? + max_len = self.max_vect_len + if max_len < np.inf: + drawn_norms = max_len * np.tanh(norms / max_len) + else: + drawn_norms = norms + + # What's the distance from the base of an arrow to + # the base of its head? + dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf) + + # Set all points + unit_outputs = np.zeros_like(outputs) + np.true_divide(outputs, norms, out=unit_outputs, where=(norms > self.min_drawn_norm)) + + points = self.get_points() + points[0::8] = samples + points[2::8] = samples + dist_to_head_base * unit_outputs + points[4::8] = points[2::8] + points[6::8] = samples + drawn_norms * unit_outputs + for i in (1, 3, 5): + points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8]) + points[7::8] = points[6:-1:8] + + # Adjust stroke widths + width_arr = self.stroke_width * self.base_stroke_width_array + width_scalars = np.clip(drawn_norms / tip_len, 0, 1) + width_scalars = np.repeat(width_scalars, 8)[:-1] + self.get_stroke_widths()[:] = width_scalars * width_arr + + # Potentially adjust opacity and color + if self.norm_to_opacity_func is not None: + self.get_stroke_opacities()[:] = self.norm_to_opacity_func( + np.repeat(norms, 8)[:-1] + ) + if self.norm_to_rgb_func is not None: + self.get_stroke_colors() + self.data['stroke_rgba'][:, :3] = self.norm_to_rgb_func( + np.repeat(norms, 8)[:-1] + ) + + self.note_changed_data() + return self + + +class TimeVaryingVectorField(VectorField): + def __init__( + self, + # Takes in an array of points and a float for time + time_func, + **kwargs + ): + self.time = 0 + super().__init__(func=lambda p: time_func(p, self.time), **kwargs) + self.add_updater(lambda m, dt: m.increment_time(dt)) + always(self.update_vectors) + + def increment_time(self, dt): + self.time += dt + + +class OldVectorField(VGroup): def __init__( self, func: Callable[[float, float], Sequence[float]], From 1f8ad5be167081293b394bd89b0e71442a2a6eda Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 21 Sep 2024 12:17:10 -0400 Subject: [PATCH 07/12] Fix pfp for null curves --- manimlib/mobject/types/vectorized_mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 0e187355..82541d70 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -766,6 +766,8 @@ class VMobject(Mobject): def quick_point_from_proportion(self, alpha: float) -> Vect3: # Assumes all curves have the same length, so is inaccurate num_curves = self.get_num_curves() + if num_curves == 0: + return self.get_center() n, residue = integer_interpolate(0, num_curves, alpha) curve_func = self.get_nth_curve_function(n) return curve_func(residue) From fea7096cbe5eb1fb77d96ed1a1bc6a429366fe11 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 28 Sep 2024 09:48:20 -0500 Subject: [PATCH 08/12] Change default animation behavior to suspend_mobject_updating=False --- manimlib/animation/animation.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index 1eadbdbd..8b897f81 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -4,6 +4,7 @@ from copy import deepcopy from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import Mobject +from manimlib.utils.iterables import remove_list_redundancies from manimlib.utils.rate_functions import smooth from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.simple_functions import clip @@ -37,7 +38,10 @@ class Animation(object): remover: bool = False, # What to enter into the update function upon completion final_alpha_value: float = 1.0, - suspend_mobject_updating: bool = True, + # If set to True, the mobject itself will have its internal updaters called, + # but the start or target mobjects would not be suspended. To completely suspend + # updating, call mobject.suspend_updating() before the animation + suspend_mobject_updating: bool = False, ): self.mobject = mobject self.run_time = run_time @@ -65,12 +69,6 @@ class Animation(object): self.mobject.set_animating_status(True) self.starting_mobject = self.create_starting_mobject() if self.suspend_mobject_updating: - # All calls to self.mobject's internal updaters - # during the animation, either from this Animation - # or from the surrounding scene, should do nothing. - # It is, however, okay and desirable to call - # the internal updaters of self.starting_mobject, - # or any others among self.get_all_mobjects() self.mobject_was_updating = not self.mobject.updating_suspended self.mobject.suspend_updating() self.families = list(self.get_all_families_zipped()) @@ -105,23 +103,19 @@ class Animation(object): def update_mobjects(self, dt: float) -> None: """ Updates things like starting_mobject, and (for - Transforms) target_mobject. Note, since typically - (always?) self.mobject will have its updating - suspended during the animation, this will do - nothing to self.mobject. + Transforms) target_mobject. """ for mob in self.get_all_mobjects_to_update(): mob.update(dt) def get_all_mobjects_to_update(self) -> list[Mobject]: # The surrounding scene typically handles - # updating of self.mobject. Besides, in - # most cases its updating is suspended anyway + # updating of self.mobject. items = list(filter( lambda m: m is not self.mobject, self.get_all_mobjects() )) - items = list(set(items)) + items = remove_list_redundancies(items) return items def copy(self): From bddd9c35eaf27e70d5c9d4770df6dd1aa9eeab73 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 1 Oct 2024 13:04:50 -0500 Subject: [PATCH 09/12] Tiny formatting --- manimlib/mobject/svg/special_tex.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/mobject/svg/special_tex.py b/manimlib/mobject/svg/special_tex.py index 578c00e2..a2ed9964 100644 --- a/manimlib/mobject/svg/special_tex.py +++ b/manimlib/mobject/svg/special_tex.py @@ -15,7 +15,6 @@ if TYPE_CHECKING: from manimlib.typing import ManimColor, Vect3 - class BulletedList(VGroup): def __init__( self, From cf37f34e1fdca0f39af8dd9de4e3b7de4453a978 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 1 Oct 2024 13:05:00 -0500 Subject: [PATCH 10/12] Add copy cursor position option --- manimlib/scene/interactive_scene.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 7c32f2f6..ea218bde 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -507,6 +507,8 @@ class InteractiveScene(Scene): self.save_selection_to_file() elif char == "d" and modifiers == SHIFT_MODIFIER: self.copy_frame_positioning() + elif char == "c" and modifiers == SHIFT_MODIFIER: + self.copy_cursor_position() elif symbol in ARROW_SYMBOLS: self.nudge_selection( vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)], @@ -631,3 +633,6 @@ class InteractiveScene(Scene): call += ", {:.2f}".format(height) call += ")" pyperclip.copy(call) + + def copy_cursor_position(self): + pyperclip.copy(str(tuple(self.mouse_point.get_center().round(2)))) From 29cb6f76fe89be8ede82b3c82ebf0e93c025b3ef Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 2 Oct 2024 07:24:33 -0500 Subject: [PATCH 11/12] Write scene insertions into a subdirectory --- manimlib/scene/scene_file_writer.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 2059b744..5d7b0d2d 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -9,6 +9,7 @@ import sys import numpy as np from pydub import AudioSegment from tqdm.auto import tqdm as ProgressDisplay +from pathlib import Path from manimlib.constants import FFMPEG_BIN from manimlib.logger import log @@ -299,15 +300,21 @@ class SceneFileWriter(object): self.video_codec = "libx264rgb" self.pixel_format = "rgb32" + def get_insert_file_path(self, index: int) -> Path: + movie_path = Path(self.get_movie_file_path()) + scene_name = movie_path.stem + insert_dir = Path(movie_path.parent, "inserts") + guarantee_existence(str(insert_dir)) + return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}") + def begin_insert(self): # Begin writing process self.write_to_movie = True self.init_output_directories() - movie_path = self.get_movie_file_path() - count = 0 - while os.path.exists(name := movie_path.replace(".", f"_insert_{count}.")): - count += 1 - self.inserted_file_path = name + index = 0 + while (insert_path := self.get_insert_file_path(index)).exists(): + index += 1 + self.inserted_file_path = str(insert_path) self.open_movie_pipe(self.inserted_file_path) def end_insert(self): From 154a473a128b001c641649c3f9f0cd769d53cbf2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 10 Oct 2024 14:05:43 -0500 Subject: [PATCH 12/12] Get rid of globals update locals hack It seems this issues is no longer there in the case of list constructors(?). Although it still exists for functions defined within a cell, that can be circumvented with more explicit function arguments. --- manimlib/scene/scene.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index f73fb088..5e3565fc 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -255,17 +255,6 @@ class Scene(object): pt_inputhooks.register("manim", inputhook) shell.enable_gui("manim") - # This is hacky, but there's an issue with ipython which is that - # when you define lambda's or list comprehensions during a shell session, - # they are not aware of local variables in the surrounding scope. Because - # That comes up a fair bit during scene construction, to get around this, - # we (admittedly sketchily) update the global namespace to match the local - # namespace, since this is just a shell session anyway. - shell.events.register( - "pre_run_cell", - lambda *args, **kwargs: shell.user_global_ns.update(shell.user_ns) - ) - # Operation to run after each ipython command def post_cell_func(*args, **kwargs): if not self.is_window_closing():