From f46e116b4cbd38d95d4b5bf768bdd5206a22d589 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Wed, 10 Apr 2024 23:09:58 -0400 Subject: [PATCH 01/36] try adding a render manager --- manim/__init__.py | 2 - manim/_config/utils.py | 10 + manim/animation/animation.py | 12 +- manim/camera/cairo_camera.py | 1213 ---------------------- manim/camera/camera.py | 3 +- manim/cli/render/global_options.py | 5 + manim/mobject/opengl/opengl_mobject.py | 7 +- manim/renderer/opengl_file_writer.py | 471 +++++++++ manim/renderer/opengl_renderer.py | 11 +- manim/renderer/opengl_renderer_window.py | 7 +- manim/renderer/render_manager.py | 90 ++ manim/scene/scene.py | 112 +- manim/scene/scene_file_writer.py | 4 +- manim/utils/commands.py | 4 +- manim/utils/hashing.py | 2 +- 15 files changed, 669 insertions(+), 1284 deletions(-) delete mode 100644 manim/camera/cairo_camera.py create mode 100644 manim/renderer/opengl_file_writer.py create mode 100644 manim/renderer/render_manager.py diff --git a/manim/__init__.py b/manim/__init__.py index f66a5f75f0..32b831089b 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -37,7 +37,6 @@ from .animation.transform_matching_parts import * from .animation.updaters.mobject_update_utils import * from .animation.updaters.update import * -from .camera.cairo_camera import * from .constants import * from .mobject.frame import * from .mobject.geometry.arc import * @@ -73,7 +72,6 @@ from .mobject.types.vectorized_mobject import * from .mobject.value_tracker import * from .mobject.vector_field import * -from .renderer.cairo_renderer import * from .scene.scene import * from .scene.scene_file_writer import * from .scene.section import * diff --git a/manim/_config/utils.py b/manim/_config/utils.py index 35b862d749..db1a17093c 100644 --- a/manim/_config/utils.py +++ b/manim/_config/utils.py @@ -311,6 +311,7 @@ class MyScene(Scene): "write_to_movie", "zero_pad", "force_window", + "parallel" } def __init__(self) -> None: @@ -582,6 +583,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> ManimConfig: "use_projection_stroke_shaders", "enable_wireframe", "force_window", + "parallel" ]: setattr(self, key, parser["CLI"].getboolean(key, fallback=False)) @@ -995,6 +997,14 @@ def format(self, val: str) -> None: "Output format set as webm, this can be slower than other formats", ) + @property + def in_parallel(self) -> None: + return self._d["parallel"] + + @in_parallel.setter + def in_parallel(self, val: bool) -> None: + self._set_boolean("parallel", val) + ffmpeg_loglevel = property( lambda self: self._d["ffmpeg_loglevel"], lambda self, val: self._set_from_list( diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 47e86ec0a7..53f8fea5d4 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -127,7 +127,7 @@ def __new__( def __init__( self, - mobject: Mobject | None, + mobject: OpenGLMobject | None, lag_ratio: float = DEFAULT_ANIMATION_LAG_RATIO, run_time: float = DEFAULT_ANIMATION_RUN_TIME, rate_func: Callable[[float], float] = smooth, @@ -153,11 +153,11 @@ def __init__( if config["renderer"] == RendererType.OPENGL: self.starting_mobject: OpenGLMobject = OpenGLMobject() self.mobject: OpenGLMobject = ( - mobject if mobject is not None else OpenGLMobject() + mobject.copy() if mobject is not None else OpenGLMobject() ) - else: - self.starting_mobject: Mobject = Mobject() - self.mobject: Mobject = mobject if mobject is not None else Mobject() + # else: + # self.starting_mobject: Mobject = Mobject() + # self.mobject: Mobject = mobject if mobject is not None else Mobject() if kwargs: logger.debug("Animation received extra kwargs: %s", kwargs) @@ -169,7 +169,7 @@ def __init__( ), ) - def _typecheck_input(self, mobject: Mobject | None) -> None: + def _typecheck_input(self, mobject: Mobject | OpenGLMobject | None) -> None: if mobject is None: logger.debug("Animation with empty mobject") elif not isinstance(mobject, (Mobject, OpenGLMobject)): diff --git a/manim/camera/cairo_camera.py b/manim/camera/cairo_camera.py deleted file mode 100644 index 9b1671ecce..0000000000 --- a/manim/camera/cairo_camera.py +++ /dev/null @@ -1,1213 +0,0 @@ -"""A camera converts the mobjects contained in a Scene into an array of pixels.""" - - -from __future__ import annotations - -__all__ = ["CairoCamera"] - -import copy -import itertools as it -import operator as op -import pathlib -import time -from functools import reduce -from typing import Any, Callable, Iterable - -import cairo -import numpy as np -from PIL import Image -from scipy.spatial.distance import pdist - -from .. import config, logger -from ..constants import * -from ..mobject.mobject import Mobject -from ..mobject.types.image_mobject import AbstractImageMobject -from ..mobject.types.point_cloud_mobject import PMobject -from ..mobject.types.vectorized_mobject import VMobject -from ..utils.color import color_to_int_rgba -from ..utils.family import extract_mobject_family_members -from ..utils.images import get_full_raster_image_path -from ..utils.iterables import list_difference_update -from ..utils.space_ops import angle_of_vector - -LINE_JOIN_MAP = { - LineJointType.AUTO: None, # TODO: this could be improved - LineJointType.ROUND: cairo.LineJoin.ROUND, - LineJointType.BEVEL: cairo.LineJoin.BEVEL, - LineJointType.MITER: cairo.LineJoin.MITER, -} - - -class CairoCamera: - """Cairo rendering implementation. - - This is the object which takes care of what exactly is displayed - on screen at any given moment. - - Parameters - ---------- - background_image - The path to an image that should be the background image. - If not set, the background is filled with :attr:`self.background_color` - background - What :attr:`background` is set to. By default, ``None``. - pixel_height - The height of the scene in pixels. - pixel_width - The width of the scene in pixels. - kwargs - Additional arguments (``background_color``, ``background_opacity``) - to be set. - """ - - def __init__( - self, - background_image: str | None = None, - frame_center: np.ndarray = ORIGIN, - image_mode: str = "RGBA", - n_channels: int = 4, - pixel_array_dtype: str = "uint8", - cairo_line_width_multiple: float = 0.01, - use_z_index: bool = True, - background: np.ndarray | None = None, - pixel_height: int | None = None, - pixel_width: int | None = None, - frame_height: float | None = None, - frame_width: float | None = None, - frame_rate: float | None = None, - **kwargs, - ): - logger.warning("Camera is deprecated. Please use OpenGLCamera instead.") - self.background_image = background_image - self.frame_center = frame_center - self.image_mode = image_mode - self.n_channels = n_channels - self.pixel_array_dtype = pixel_array_dtype - self.cairo_line_width_multiple = cairo_line_width_multiple - self.use_z_index = use_z_index - self.background = background - - if pixel_height is None: - pixel_height = config["pixel_height"] - self.pixel_height = pixel_height - - if pixel_width is None: - pixel_width = config["pixel_width"] - self.pixel_width = pixel_width - - if frame_height is None: - frame_height = config["frame_height"] - self.frame_height = frame_height - - if frame_width is None: - frame_width = config["frame_width"] - self.frame_width = frame_width - - if frame_rate is None: - frame_rate = config["frame_rate"] - self.frame_rate = frame_rate - - # TODO: change this to not use kwargs.get - for attr in ["background_color", "background_opacity"]: - setattr(self, f"_{attr}", kwargs.get(attr, config[attr])) - - # This one is in the same boat as the above, but it doesn't have the - # same name as the corresponding key so it has to be handled on its own - self.max_allowable_norm = config["frame_width"] - - self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max - self.pixel_array_to_cairo_context = {} - - # Contains the correct method to process a list of Mobjects of the - # corresponding class. If a Mobject is not an instance of a class in - # this dict (or an instance of a class that inherits from a class in - # this dict), then it cannot be rendered. - - self.init_background() - self.resize_frame_shape() - self.reset() - - def __deepcopy__(self, memo): - # This is to address a strange bug where deepcopying - # will result in a segfault, which is somehow related - # to the aggdraw library - self.canvas = None - return copy.copy(self) - - @property - def background_color(self): - return self._background_color - - @background_color.setter - def background_color(self, color): - self._background_color = color - self.init_background() - - @property - def background_opacity(self): - return self._background_opacity - - @background_opacity.setter - def background_opacity(self, alpha): - self._background_opacity = alpha - self.init_background() - - def type_or_raise(self, mobject: Mobject): - """Return the type of mobject, if it is a type that can be rendered. - - If `mobject` is an instance of a class that inherits from a class that - can be rendered, return the super class. For example, an instance of a - Square is also an instance of VMobject, and these can be rendered. - Therefore, `type_or_raise(Square())` returns True. - - Parameters - ---------- - mobject - The object to take the type of. - - Notes - ----- - For a list of classes that can currently be rendered, see :meth:`display_funcs`. - - Returns - ------- - Type[:class:`~.Mobject`] - The type of mobjects, if it can be rendered. - - Raises - ------ - :exc:`TypeError` - When mobject is not an instance of a class that can be rendered. - """ - self.display_funcs = { - VMobject: self.display_multiple_vectorized_mobjects, - PMobject: self.display_multiple_point_cloud_mobjects, - AbstractImageMobject: self.display_multiple_image_mobjects, - Mobject: lambda batch, pa: batch, # Do nothing - } - # We have to check each type in turn because we are dealing with - # super classes. For example, if square = Square(), then - # type(square) != VMobject, but isinstance(square, VMobject) == True. - for _type in self.display_funcs: - if isinstance(mobject, _type): - return _type - raise TypeError(f"Displaying an object of class {_type} is not supported") - - def reset_pixel_shape(self, new_height: float, new_width: float): - """This method resets the height and width - of a single pixel to the passed new_height and new_width. - - Parameters - ---------- - new_height - The new height of the entire scene in pixels - new_width - The new width of the entire scene in pixels - """ - self.pixel_width = new_width - self.pixel_height = new_height - self.init_background() - self.resize_frame_shape() - self.reset() - - def resize_frame_shape(self, fixed_dimension: int = 0): - """ - Changes frame_shape to match the aspect ratio - of the pixels, where fixed_dimension determines - whether frame_height or frame_width - remains fixed while the other changes accordingly. - - Parameters - ---------- - fixed_dimension - If 0, height is scaled with respect to width - else, width is scaled with respect to height. - """ - pixel_height = self.pixel_height - pixel_width = self.pixel_width - frame_height = self.frame_height - frame_width = self.frame_width - aspect_ratio = pixel_width / pixel_height - if fixed_dimension == 0: - frame_height = frame_width / aspect_ratio - else: - frame_width = aspect_ratio * frame_height - self.frame_height = frame_height - self.frame_width = frame_width - - def init_background(self): - """Initialize the background. - If self.background_image is the path of an image - the image is set as background; else, the default - background color fills the background. - """ - height = self.pixel_height - width = self.pixel_width - if self.background_image is not None: - path = get_full_raster_image_path(self.background_image) - image = Image.open(path).convert(self.image_mode) - # TODO, how to gracefully handle backgrounds - # with different sizes? - self.background = np.array(image)[:height, :width] - self.background = self.background.astype(self.pixel_array_dtype) - else: - background_rgba = color_to_int_rgba( - self.background_color, - self.background_opacity, - ) - self.background = np.zeros( - (height, width, self.n_channels), - dtype=self.pixel_array_dtype, - ) - self.background[:, :] = background_rgba - - def get_image(self, pixel_array: np.ndarray | list | tuple | None = None): - """Returns an image from the passed - pixel array, or from the current frame - if the passed pixel array is none. - - Parameters - ---------- - pixel_array - The pixel array from which to get an image, by default None - - Returns - ------- - PIL.Image - The PIL image of the array. - """ - if pixel_array is None: - pixel_array = self.pixel_array - return Image.fromarray(pixel_array, mode=self.image_mode) - - def convert_pixel_array( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): - """Converts a pixel array from values that have floats in then - to proper RGB values. - - Parameters - ---------- - pixel_array - Pixel array to convert. - convert_from_floats - Whether or not to convert float values to ints, by default False - - Returns - ------- - np.array - The new, converted pixel array. - """ - retval = np.array(pixel_array) - if convert_from_floats: - retval = np.apply_along_axis( - lambda f: (f * self.rgb_max_val).astype(self.pixel_array_dtype), - 2, - retval, - ) - return retval - - def set_pixel_array( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): - """Sets the pixel array of the camera to the passed pixel array. - - Parameters - ---------- - pixel_array - The pixel array to convert and then set as the camera's pixel array. - convert_from_floats - Whether or not to convert float values to proper RGB values, by default False - """ - converted_array = self.convert_pixel_array(pixel_array, convert_from_floats) - if not ( - hasattr(self, "pixel_array") - and self.pixel_array.shape == converted_array.shape - ): - self.pixel_array = converted_array - else: - # Set in place - self.pixel_array[:, :, :] = converted_array[:, :, :] - - def set_background( - self, pixel_array: np.ndarray | list | tuple, convert_from_floats: bool = False - ): - """Sets the background to the passed pixel_array after converting - to valid RGB values. - - Parameters - ---------- - pixel_array - The pixel array to set the background to. - convert_from_floats - Whether or not to convert floats values to proper RGB valid ones, by default False - """ - self.background = self.convert_pixel_array(pixel_array, convert_from_floats) - - # TODO, this should live in utils, not as a method of Camera - def make_background_from_func( - self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray] - ): - """ - Makes a pixel array for the background by using coords_to_colors_func to determine each pixel's color. Each input - pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not - pixel coordinates), and each output is expected to be an RGBA array of 4 floats. - - Parameters - ---------- - coords_to_colors_func - The function whose input is an (x,y) pair of coordinates and - whose return values must be the colors for that point - - Returns - ------- - np.array - The pixel array which can then be passed to set_background. - """ - - logger.info("Starting set_background") - coords = self.get_coords_of_all_pixels() - new_background = np.apply_along_axis(coords_to_colors_func, 2, coords) - logger.info("Ending set_background") - - return self.convert_pixel_array(new_background, convert_from_floats=True) - - def set_background_from_func( - self, coords_to_colors_func: Callable[[np.ndarray], np.ndarray] - ): - """ - Sets the background to a pixel array using coords_to_colors_func to determine each pixel's color. Each input - pixel's color. Each input to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not - pixel coordinates), and each output is expected to be an RGBA array of 4 floats. - - Parameters - ---------- - coords_to_colors_func - The function whose input is an (x,y) pair of coordinates and - whose return values must be the colors for that point - """ - self.set_background(self.make_background_from_func(coords_to_colors_func)) - - def reset(self): - """Resets the camera's pixel array - to that of the background - - Returns - ------- - Camera - The camera object after setting the pixel array. - """ - self.set_pixel_array(self.background) - return self - - def set_frame_to_background(self, background): - self.set_pixel_array(background) - - #### - - def get_mobjects_to_display( - self, - mobjects: Iterable[Mobject], - include_submobjects: bool = True, - excluded_mobjects: list | None = None, - ): - """Used to get the list of mobjects to display - with the camera. - - Parameters - ---------- - mobjects - The Mobjects - include_submobjects - Whether or not to include the submobjects of mobjects, by default True - excluded_mobjects - Any mobjects to exclude, by default None - - Returns - ------- - list - list of mobjects - """ - if include_submobjects: - mobjects = extract_mobject_family_members( - mobjects, - use_z_index=self.use_z_index, - only_those_with_points=True, - ) - if excluded_mobjects: - all_excluded = extract_mobject_family_members( - excluded_mobjects, - use_z_index=self.use_z_index, - ) - mobjects = list_difference_update(mobjects, all_excluded) - return list(mobjects) - - def is_in_frame(self, mobject: Mobject): - """Checks whether the passed mobject is in - frame or not. - - Parameters - ---------- - mobject - The mobject for which the checking needs to be done. - - Returns - ------- - bool - True if in frame, False otherwise. - """ - fc = self.frame_center - fh = self.frame_height - fw = self.frame_width - return not reduce( - op.or_, - [ - mobject.get_right()[0] < fc[0] - fw / 2, - mobject.get_bottom()[1] > fc[1] + fh / 2, - mobject.get_left()[0] > fc[0] + fw / 2, - mobject.get_top()[1] < fc[1] - fh / 2, - ], - ) - - def capture_mobject(self, mobject: Mobject, **kwargs: Any): - """Capture mobjects by storing it in :attr:`pixel_array`. - - This is a single-mobject version of :meth:`capture_mobjects`. - - Parameters - ---------- - mobject - Mobject to capture. - - kwargs - Keyword arguments to be passed to :meth:`get_mobjects_to_display`. - - """ - return self.capture_mobjects([mobject], **kwargs) - - def capture_mobjects(self, mobjects: Iterable[Mobject], **kwargs): - """Capture mobjects by printing them on :attr:`pixel_array`. - - This is the essential function that converts the contents of a Scene - into an array, which is then converted to an image or video. - - Parameters - ---------- - mobjects - Mobjects to capture. - - kwargs - Keyword arguments to be passed to :meth:`get_mobjects_to_display`. - - Notes - ----- - For a list of classes that can currently be rendered, see :meth:`display_funcs`. - - """ - # The mobjects will be processed in batches (or runs) of mobjects of - # the same type. That is, if the list mobjects contains objects of - # types [VMobject, VMobject, VMobject, PMobject, PMobject, VMobject], - # then they will be captured in three batches: [VMobject, VMobject, - # VMobject], [PMobject, PMobject], and [VMobject]. This must be done - # without altering their order. it.groupby computes exactly this - # partition while at the same time preserving order. - mobjects = self.get_mobjects_to_display(mobjects, **kwargs) - for group_type, group in it.groupby(mobjects, self.type_or_raise): - self.display_funcs[group_type](list(group), self.pixel_array) - - # Methods associated with svg rendering - - # NOTE: None of the methods below have been mentioned outside of their definitions. Their DocStrings are not as - # detailed as possible. - - def get_cached_cairo_context(self, pixel_array: np.ndarray): - """Returns the cached cairo context of the passed - pixel array if it exists, and None if it doesn't. - - Parameters - ---------- - pixel_array - The pixel array to check. - - Returns - ------- - cairo.Context - The cached cairo context. - """ - return self.pixel_array_to_cairo_context.get(id(pixel_array), None) - - def cache_cairo_context(self, pixel_array: np.ndarray, ctx: cairo.Context): - """Caches the passed Pixel array into a Cairo Context - - Parameters - ---------- - pixel_array - The pixel array to cache - ctx - The context to cache it into. - """ - self.pixel_array_to_cairo_context[id(pixel_array)] = ctx - - def get_cairo_context(self, pixel_array: np.ndarray): - """Returns the cairo context for a pixel array after - caching it to self.pixel_array_to_cairo_context - If that array has already been cached, it returns the - cached version instead. - - Parameters - ---------- - pixel_array - The Pixel array to get the cairo context of. - - Returns - ------- - cairo.Context - The cairo context of the pixel array. - """ - cached_ctx = self.get_cached_cairo_context(pixel_array) - if cached_ctx: - return cached_ctx - pw = self.pixel_width - ph = self.pixel_height - fw = self.frame_width - fh = self.frame_height - fc = self.frame_center - surface = cairo.ImageSurface.create_for_data( - pixel_array, - cairo.FORMAT_ARGB32, - pw, - ph, - ) - ctx = cairo.Context(surface) - ctx.scale(pw, ph) - ctx.set_matrix( - cairo.Matrix( - (pw / fw), - 0, - 0, - -(ph / fh), - (pw / 2) - fc[0] * (pw / fw), - (ph / 2) + fc[1] * (ph / fh), - ), - ) - self.cache_cairo_context(pixel_array, ctx) - return ctx - - def display_multiple_vectorized_mobjects( - self, vmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple VMobjects in the pixel_array - - Parameters - ---------- - vmobjects - list of VMobjects to display - pixel_array - The pixel array - """ - if len(vmobjects) == 0: - return - batch_image_pairs = it.groupby(vmobjects, lambda vm: vm.get_background_image()) - for image, batch in batch_image_pairs: - if image: - self.display_multiple_background_colored_vmobjects(batch, pixel_array) - else: - self.display_multiple_non_background_colored_vmobjects( - batch, - pixel_array, - ) - - def display_multiple_non_background_colored_vmobjects( - self, vmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple VMobjects in the cairo context, as long as they don't have - background colors. - - Parameters - ---------- - vmobjects - list of the VMobjects - pixel_array - The Pixel array to add the VMobjects to. - """ - ctx = self.get_cairo_context(pixel_array) - for vmobject in vmobjects: - self.display_vectorized(vmobject, ctx) - - def display_vectorized(self, vmobject: VMobject, ctx: cairo.Context): - """Displays a VMobject in the cairo context - - Parameters - ---------- - vmobject - The Vectorized Mobject to display - ctx - The cairo context to use. - - Returns - ------- - Camera - The camera object - """ - self.set_cairo_context_path(ctx, vmobject) - self.apply_stroke(ctx, vmobject, background=True) - self.apply_fill(ctx, vmobject) - self.apply_stroke(ctx, vmobject) - return self - - def set_cairo_context_path(self, ctx: cairo.Context, vmobject: VMobject): - """Sets a path for the cairo context with the vmobject passed - - Parameters - ---------- - ctx - The cairo context - vmobject - The VMobject - - Returns - ------- - Camera - Camera object after setting cairo_context_path - """ - points = self.transform_points_pre_display(vmobject, vmobject.points) - # TODO, shouldn't this be handled in transform_points_pre_display? - # points = points - self.get_frame_center() - if len(points) == 0: - return - - ctx.new_path() - subpaths = vmobject.gen_subpaths_from_points_2d(points) - for subpath in subpaths: - quads = vmobject.gen_cubic_bezier_tuples_from_points(subpath) - ctx.new_sub_path() - start = subpath[0] - ctx.move_to(*start[:2]) - for _p0, p1, p2, p3 in quads: - ctx.curve_to(*p1[:2], *p2[:2], *p3[:2]) - if vmobject.consider_points_equals_2d(subpath[0], subpath[-1]): - ctx.close_path() - return self - - def set_cairo_context_color( - self, ctx: cairo.Context, rgbas: np.ndarray, vmobject: VMobject - ): - """Sets the color of the cairo context - - Parameters - ---------- - ctx - The cairo context - rgbas - The RGBA array with which to color the context. - vmobject - The VMobject with which to set the color. - - Returns - ------- - Camera - The camera object - """ - if len(rgbas) == 1: - # Use reversed rgb because cairo surface is - # encodes it in reverse order - ctx.set_source_rgba(*rgbas[0][2::-1], rgbas[0][3]) - else: - points = vmobject.get_gradient_start_and_end_points() - points = self.transform_points_pre_display(vmobject, points) - pat = cairo.LinearGradient(*it.chain(*(point[:2] for point in points))) - step = 1.0 / (len(rgbas) - 1) - offsets = np.arange(0, 1 + step, step) - for rgba, offset in zip(rgbas, offsets): - pat.add_color_stop_rgba(offset, *rgba[2::-1], rgba[3]) - ctx.set_source(pat) - return self - - def apply_fill(self, ctx: cairo.Context, vmobject: VMobject): - """Fills the cairo context - - Parameters - ---------- - ctx - The cairo context - vmobject - The VMobject - - Returns - ------- - Camera - The camera object. - """ - self.set_cairo_context_color(ctx, self.get_fill_rgbas(vmobject), vmobject) - ctx.fill_preserve() - return self - - def apply_stroke( - self, ctx: cairo.Context, vmobject: VMobject, background: bool = False - ): - """Applies a stroke to the VMobject in the cairo context. - - Parameters - ---------- - ctx - The cairo context - vmobject - The VMobject - background - Whether or not to consider the background when applying this - stroke width, by default False - - Returns - ------- - Camera - The camera object with the stroke applied. - """ - width = vmobject.get_stroke_width(background) - if width == 0: - return self - self.set_cairo_context_color( - ctx, - self.get_stroke_rgbas(vmobject, background=background), - vmobject, - ) - ctx.set_line_width( - width - * self.cairo_line_width_multiple - # This ensures lines have constant width as you zoom in on them. - * (self.frame_width / self.frame_width), - ) - if vmobject.joint_type != LineJointType.AUTO: - ctx.set_line_join(LINE_JOIN_MAP[vmobject.joint_type]) - ctx.stroke_preserve() - return self - - def get_stroke_rgbas(self, vmobject: VMobject, background: bool = False): - """Gets the RGBA array for the stroke of the passed - VMobject. - - Parameters - ---------- - vmobject - The VMobject - background - Whether or not to consider the background when getting the stroke - RGBAs, by default False - - Returns - ------- - np.ndarray - The RGBA array of the stroke. - """ - return vmobject.get_stroke_rgbas(background) - - def get_fill_rgbas(self, vmobject: VMobject): - """Returns the RGBA array of the fill of the passed VMobject - - Parameters - ---------- - vmobject - The VMobject - - Returns - ------- - np.array - The RGBA Array of the fill of the VMobject - """ - return vmobject.get_fill_rgbas() - - def get_background_colored_vmobject_displayer(self): - """Returns the background_colored_vmobject_displayer - if it exists or makes one and returns it if not. - - Returns - ------- - BackGroundColoredVMobjectDisplayer - Object that displays VMobjects that have the same color - as the background. - """ - # Quite wordy to type out a bunch - bcvd = "background_colored_vmobject_displayer" - if not hasattr(self, bcvd): - setattr(self, bcvd, BackgroundColoredVMobjectDisplayer(self)) - return getattr(self, bcvd) - - def display_multiple_background_colored_vmobjects( - self, cvmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple vmobjects that have the same color as the background. - - Parameters - ---------- - cvmobjects - List of Colored VMobjects - pixel_array - The pixel array. - - Returns - ------- - Camera - The camera object. - """ - displayer = self.get_background_colored_vmobject_displayer() - cvmobject_pixel_array = displayer.display(*cvmobjects) - self.overlay_rgba_array(pixel_array, cvmobject_pixel_array) - return self - - # Methods for other rendering - - # NOTE: Out of the following methods, only `transform_points_pre_display` and `points_to_pixel_coords` have been mentioned outside of their definitions. - # As a result, the other methods do not have as detailed docstrings as would be preferred. - - def display_multiple_point_cloud_mobjects( - self, pmobjects: list, pixel_array: np.ndarray - ): - """Displays multiple PMobjects by modifying the passed pixel array. - - Parameters - ---------- - pmobjects - List of PMobjects - pixel_array - The pixel array to modify. - """ - for pmobject in pmobjects: - self.display_point_cloud( - pmobject, - pmobject.points, - pmobject.rgbas, - self.adjusted_thickness(pmobject.stroke_width), - pixel_array, - ) - - def display_point_cloud( - self, - pmobject: PMobject, - points: list, - rgbas: np.ndarray, - thickness: float, - pixel_array: np.ndarray, - ): - """Displays a PMobject by modifying the Pixel array suitably.. - TODO: Write a description for the rgbas argument. - Parameters - ---------- - pmobject - Point Cloud Mobject - points - The points to display in the point cloud mobject - rgbas - - thickness - The thickness of each point of the PMobject - pixel_array - The pixel array to modify. - - """ - if len(points) == 0: - return - pixel_coords = self.points_to_pixel_coords(pmobject, points) - pixel_coords = self.thickened_coordinates(pixel_coords, thickness) - rgba_len = pixel_array.shape[2] - - rgbas = (self.rgb_max_val * rgbas).astype(self.pixel_array_dtype) - target_len = len(pixel_coords) - factor = target_len // len(rgbas) - rgbas = np.array([rgbas] * factor).reshape((target_len, rgba_len)) - - on_screen_indices = self.on_screen_pixels(pixel_coords) - pixel_coords = pixel_coords[on_screen_indices] - rgbas = rgbas[on_screen_indices] - - ph = self.pixel_height - pw = self.pixel_width - - flattener = np.array([1, pw], dtype="int") - flattener = flattener.reshape((2, 1)) - indices = np.dot(pixel_coords, flattener)[:, 0] - indices = indices.astype("int") - - new_pa = pixel_array.reshape((ph * pw, rgba_len)) - new_pa[indices] = rgbas - pixel_array[:, :] = new_pa.reshape((ph, pw, rgba_len)) - - def display_multiple_image_mobjects( - self, image_mobjects: list, pixel_array: np.ndarray - ): - """Displays multiple image mobjects by modifying the passed pixel_array. - - Parameters - ---------- - image_mobjects - list of ImageMobjects - pixel_array - The pixel array to modify. - """ - for image_mobject in image_mobjects: - self.display_image_mobject(image_mobject, pixel_array) - - def display_image_mobject( - self, image_mobject: AbstractImageMobject, pixel_array: np.ndarray - ): - """Displays an ImageMobject by changing the pixel_array suitably. - - Parameters - ---------- - image_mobject - The imageMobject to display - pixel_array - The Pixel array to put the imagemobject in. - """ - corner_coords = self.points_to_pixel_coords(image_mobject, image_mobject.points) - ul_coords, ur_coords, dl_coords = corner_coords - right_vect = ur_coords - ul_coords - down_vect = dl_coords - ul_coords - center_coords = ul_coords + (right_vect + down_vect) / 2 - - sub_image = Image.fromarray(image_mobject.get_pixel_array(), mode="RGBA") - - # Reshape - pixel_width = max(int(pdist([ul_coords, ur_coords])), 1) - pixel_height = max(int(pdist([ul_coords, dl_coords])), 1) - sub_image = sub_image.resize( - (pixel_width, pixel_height), - resample=image_mobject.resampling_algorithm, - ) - - # Rotate - angle = angle_of_vector(right_vect) - adjusted_angle = -int(360 * angle / TAU) - if adjusted_angle != 0: - sub_image = sub_image.rotate( - adjusted_angle, - resample=image_mobject.resampling_algorithm, - expand=1, - ) - - # TODO, there is no accounting for a shear... - - # Paste into an image as large as the camera's pixel array - full_image = Image.fromarray( - np.zeros((self.pixel_height, self.pixel_width)), - mode="RGBA", - ) - new_ul_coords = center_coords - np.array(sub_image.size) / 2 - new_ul_coords = new_ul_coords.astype(int) - full_image.paste( - sub_image, - box=( - new_ul_coords[0], - new_ul_coords[1], - new_ul_coords[0] + sub_image.size[0], - new_ul_coords[1] + sub_image.size[1], - ), - ) - # Paint on top of existing pixel array - self.overlay_PIL_image(pixel_array, full_image) - - def overlay_rgba_array(self, pixel_array: np.ndarray, new_array: np.ndarray): - """Overlays an RGBA array on top of the given Pixel array. - - Parameters - ---------- - pixel_array - The original pixel array to modify. - new_array - The new pixel array to overlay. - """ - self.overlay_PIL_image(pixel_array, self.get_image(new_array)) - - def overlay_PIL_image(self, pixel_array: np.ndarray, image: Image): - """Overlays a PIL image on the passed pixel array. - - Parameters - ---------- - pixel_array - The Pixel array - image - The Image to overlay. - """ - pixel_array[:, :] = np.array( - Image.alpha_composite(self.get_image(pixel_array), image), - dtype="uint8", - ) - - def adjust_out_of_range_points(self, points: np.ndarray): - """If any of the points in the passed array are out of - the viable range, they are adjusted suitably. - - Parameters - ---------- - points - The points to adjust - - Returns - ------- - np.array - The adjusted points. - """ - if not np.any(points > self.max_allowable_norm): - return points - norms = np.apply_along_axis(np.linalg.norm, 1, points) - violator_indices = norms > self.max_allowable_norm - violators = points[violator_indices, :] - violator_norms = norms[violator_indices] - reshaped_norms = np.repeat( - violator_norms.reshape((len(violator_norms), 1)), - points.shape[1], - 1, - ) - rescaled = self.max_allowable_norm * violators / reshaped_norms - points[violator_indices] = rescaled - return points - - def transform_points_pre_display( - self, - mobject, - points, - ): # TODO: Write more detailed docstrings for this method. - # NOTE: There seems to be an unused argument `mobject`. - - # Subclasses (like ThreeDCamera) may want to - # adjust points further before they're shown - if not np.all(np.isfinite(points)): - # TODO, print some kind of warning about - # mobject having invalid points? - points = np.zeros((1, 3)) - return points - - def points_to_pixel_coords( - self, - mobject, - points, - ): # TODO: Write more detailed docstrings for this method. - points = self.transform_points_pre_display(mobject, points) - shifted_points = points - self.frame_center - - result = np.zeros((len(points), 2)) - pixel_height = self.pixel_height - pixel_width = self.pixel_width - frame_height = self.frame_height - frame_width = self.frame_width - width_mult = pixel_width / frame_width - width_add = pixel_width / 2 - height_mult = pixel_height / frame_height - height_add = pixel_height / 2 - # Flip on y-axis as you go - height_mult *= -1 - - result[:, 0] = shifted_points[:, 0] * width_mult + width_add - result[:, 1] = shifted_points[:, 1] * height_mult + height_add - return result.astype("int") - - def on_screen_pixels(self, pixel_coords: np.ndarray): - """Returns array of pixels that are on the screen from a given - array of pixel_coordinates - - Parameters - ---------- - pixel_coords - The pixel coords to check. - - Returns - ------- - np.array - The pixel coords on screen. - """ - return reduce( - op.and_, - [ - pixel_coords[:, 0] >= 0, - pixel_coords[:, 0] < self.pixel_width, - pixel_coords[:, 1] >= 0, - pixel_coords[:, 1] < self.pixel_height, - ], - ) - - def adjusted_thickness(self, thickness: float): - """ - - Parameters - ---------- - thickness - - Returns - ------- - float - - """ - # TODO: This seems...unsystematic - big_sum = op.add(config["pixel_height"], config["pixel_width"]) - this_sum = op.add(self.pixel_height, self.pixel_width) - factor = big_sum / this_sum - return 1 + (thickness - 1) * factor - - def get_thickening_nudges(self, thickness: float): - """ - - Parameters - ---------- - thickness - - Returns - ------- - np.array - - """ - thickness = int(thickness) - _range = list(range(-thickness // 2 + 1, thickness // 2 + 1)) - return np.array(list(it.product(_range, _range))) - - def thickened_coordinates(self, pixel_coords: np.ndarray, thickness: float): - """Returns thickened coordinates for a passed array of pixel coords and - a thickness to thicken by. - - Parameters - ---------- - pixel_coords - Pixel coordinates - thickness - Thickness - - Returns - ------- - np.array - Array of thickened pixel coords. - """ - nudges = self.get_thickening_nudges(thickness) - pixel_coords = np.array([pixel_coords + nudge for nudge in nudges]) - size = pixel_coords.size - return pixel_coords.reshape((size // 2, 2)) - - # TODO, reimplement using cairo matrix - def get_coords_of_all_pixels(self): - """Returns the cartesian coordinates of each pixel. - - Returns - ------- - np.ndarray - The array of cartesian coordinates. - """ - # These are in x, y order, to help me keep things straight - full_space_dims = np.array([self.frame_width, self.frame_height]) - full_pixel_dims = np.array([self.pixel_width, self.pixel_height]) - - # These are addressed in the same y, x order as in pixel_array, but the values in them - # are listed in x, y order - uncentered_pixel_coords = np.indices([self.pixel_height, self.pixel_width])[ - ::-1 - ].transpose(1, 2, 0) - uncentered_space_coords = ( - uncentered_pixel_coords * full_space_dims - ) / full_pixel_dims - # Could structure above line's computation slightly differently, but figured (without much - # thought) multiplying by frame_shape first, THEN dividing by pixel_shape, is probably - # better than the other order, for avoiding underflow quantization in the division (whereas - # overflow is unlikely to be a problem) - - centered_space_coords = uncentered_space_coords - (full_space_dims / 2) - - # Have to also flip the y coordinates to account for pixel array being listed in - # top-to-bottom order, opposite of screen coordinate convention - centered_space_coords = centered_space_coords * (1, -1) - - return centered_space_coords diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 413ce60c89..47b8aba6de 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -30,8 +30,7 @@ from ..utils.space_ops import normalize -# TODO: This becomes the new camera in the future -class OpenGLCameraFrame(OpenGLMobject): +class Camera(OpenGLMobject): def __init__( self, frame_shape: tuple[float, float] = (config.frame_width, config.frame_height), diff --git a/manim/cli/render/global_options.py b/manim/cli/render/global_options.py index b17e4671c2..2ad3c2f28f 100644 --- a/manim/cli/render/global_options.py +++ b/manim/cli/render/global_options.py @@ -102,4 +102,9 @@ def validate_gui_location(ctx, param, value): help="Renders animations without outputting image or video files and disables the window", default=False, ), + option( + "--parallel", + default=True, + help="Renders all animations in parallel" + ) ) diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py index 5de786e785..391f87c3bc 100644 --- a/manim/mobject/opengl/opengl_mobject.py +++ b/manim/mobject/opengl/opengl_mobject.py @@ -48,12 +48,12 @@ ) if TYPE_CHECKING: - from typing import Callable, Iterable, Sequence, Tuple, Union + from typing import Any, Callable, Iterable, Sequence, Union from typing_extensions import Self, TypeAlias TimeBasedUpdater: TypeAlias = Callable[ - ["OpenGLMobject", float], "OpenGLMobject" | None + ["OpenGLMobject", float], "OpenGLMobject | None" ] NonTimeUpdater: TypeAlias = Callable[["OpenGLMobject"], "OpenGLMobject" | None] Updater: TypeAlias = Union[TimeBasedUpdater, NonTimeUpdater] @@ -61,11 +61,12 @@ from manim.renderer.renderer import RendererData T = TypeVar("T", bound=RendererData) + _F = TypeVar("_F", bound=Callable[..., Any]) UNIFORM_DTYPE = np.float64 -def stash_mobject_pointers(func: Callable): +def stash_mobject_pointers(func: _F) -> _F: @wraps(func) def wrapper(self, *args, **kwargs): uncopied_attrs = ["parents", "target", "saved_state"] diff --git a/manim/renderer/opengl_file_writer.py b/manim/renderer/opengl_file_writer.py new file mode 100644 index 0000000000..a1a4de94fd --- /dev/null +++ b/manim/renderer/opengl_file_writer.py @@ -0,0 +1,471 @@ +from __future__ import annotations + +import os +import platform +import shutil +import subprocess as sp +import sys +from pathlib import Path +import pathlib as path +from typing import TYPE_CHECKING + +import numpy as np +from pydub import AudioSegment +from tqdm import tqdm as ProgressDisplay + +from manim import config +from manim._config import logger as log +from manim.mobject.mobject import Mobject +from manim.utils.file_ops import ( + add_extension_if_not_present, + get_sorted_integer_files, + guarantee_existence, +) +from manim.utils.sounds import get_full_sound_file_path + +if TYPE_CHECKING: + from PIL.Image import Image + + from manim.camera.camera import OpenGLCamera as Camera + + +class FileWriter: + def __init__( + self, + file_name: str, + write_to_movie: bool = False, + break_into_partial_movies: bool = False, + save_pngs: bool = False, # TODO, this currently does nothing + png_mode: str = "RGBA", + save_last_frame: bool = False, + movie_file_extension: str = ".mp4", + # What python file is generating this scene + input_file_path: str = "", + # Where should this be written + output_directory: str | None = None, + open_file_upon_completion: bool = False, + show_file_location_upon_completion: bool = False, + quiet: bool = False, + total_frames: int = 0, + progress_description_len: int = 40, + ): + self.frames: list[Image] = [] + self.write_to_movie = write_to_movie + self.break_into_partial_movies = break_into_partial_movies + self.save_pngs = save_pngs + self.png_mode = png_mode + self.save_last_frame = save_last_frame + self.movie_file_extension = movie_file_extension + self.input_file_path = input_file_path + self.output_directory = output_directory + self.file_name = file_name + self.open_file_upon_completion = open_file_upon_completion + self.show_file_location_upon_completion = show_file_location_upon_completion + self.quiet = quiet + self.total_frames = total_frames + self.progress_description_len = progress_description_len + + # State during file writing + self.writing_process: sp.Popen | None = None + self.progress_display: ProgressDisplay | None = None + self.ended_with_interrupt: bool = False + self.init_output_directories() + self.init_audio() + + # Output directories and files + def init_output_directories(self) -> None: + out_dir = self.output_directory or "" + scene_name = Path(self.file_name) + if self.save_last_frame: + image_dir = guarantee_existence(Path(out_dir) / "images") + image_file = add_extension_if_not_present(scene_name, ".png") + self.image_file_path = Path(image_dir) / image_file + if self.write_to_movie: + movie_dir = guarantee_existence(Path(out_dir) / "videos") + movie_file = add_extension_if_not_present( + scene_name, self.movie_file_extension + ) + self.movie_file_path = Path(movie_dir) / movie_file + if self.break_into_partial_movies: + self.partial_movie_directory = guarantee_existence( + Path(movie_dir) / "partial_movie_files" / scene_name, + ) + # A place to save mobjects + self.saved_mobject_directory = Path(out_dir) / "mobjects" / scene_name + + def add_frames(self, *frames: Image) -> None: + self.frames.extend(frames) + + def get_default_module_directory(self) -> str: + path, _ = os.path.splitext(self.input_file_path) + path = path.removeprefix("_") + return path + + # Directory getters + def get_image_file_path(self) -> str: + return self.image_file_path + + # Sound + def init_audio(self) -> None: + self.includes_sound: bool = False + + def create_audio_segment(self) -> None: + self.audio_segment = AudioSegment.silent() + + def add_audio_segment( + self, + new_segment: AudioSegment, + time: float | None = None, + gain_to_background: float | None = None, + ) -> None: + if not self.includes_sound: + self.includes_sound = True + self.create_audio_segment() + segment = self.audio_segment + curr_end = segment.duration_seconds + if time is None: + time = curr_end + if time < 0: + raise Exception("Adding sound at timestamp < 0") + + new_end = time + new_segment.duration_seconds + diff = new_end - curr_end + if diff > 0: + segment = segment.append( + AudioSegment.silent(int(np.ceil(diff * 1000))), + crossfade=0, + ) + self.audio_segment = segment.overlay( + new_segment, + position=int(1000 * time), + gain_during_overlay=gain_to_background, + ) + + def add_sound( + self, + sound_file: str, + time: float | None = None, + gain: float | None = None, + gain_to_background: float | None = None, + ) -> None: + file_path = get_full_sound_file_path(sound_file) + new_segment = AudioSegment.from_file(file_path) + if gain: + new_segment = new_segment.apply_gain(gain) + self.add_audio_segment(new_segment, time, gain_to_background) + + # Writers + def begin(self) -> None: + if not self.break_into_partial_movies and self.write_to_movie: + self.open_movie_pipe(self.get_movie_file_path()) + + def begin_animation(self) -> None: + if self.break_into_partial_movies and self.write_to_movie: + # self.open_movie_pipe(self.get_next_partial_movie_path()) + ... + + def end_animation(self) -> None: + if self.break_into_partial_movies and self.write_to_movie: + # self.close_movie_pipe() + ... + + def finish(self) -> None: + if self.write_to_movie: + if self.break_into_partial_movies: + self.combine_movie_files() + else: + self.close_movie_pipe() + if self.includes_sound: + self.add_sound_to_video() + self.print_file_ready_message(self.get_movie_file_path()) + if self.save_last_frame: + self.save_final_image(self.scene.get_image()) + if self.should_open_file(): + self.open_file() + + def open_movie_pipe(self, file_path: str) -> None: + stem, ext = os.path.splitext(file_path) + self.final_file_path = file_path + self.temp_file_path = stem + "_temp" + ext + + fps = self.scene.camera.fps + width, height = self.scene.camera.get_pixel_shape() + + command = [ + config.ffmpeg_executable, + "-y", # overwrite output file if it exists + "-f", + "rawvideo", + "-s", + f"{width}x{height}", # size of one frame + "-pix_fmt", + "rgba", + "-r", + str(fps), # frames per second + "-i", + "-", # The input comes from a pipe + "-vf", + "vflip", + "-an", # Tells FFMPEG not to expect any audio + "-loglevel", + "error", + ] + if self.movie_file_extension == ".mov": + # This is if the background of the exported + # video should be transparent. + command += [ + "-vcodec", + "prores_ks", + ] + elif self.movie_file_extension != ".gif": + command += [ + "-vcodec", + "libx264", + "-pix_fmt", + "yuv420p", + ] + command += [self.temp_file_path] + self.writing_process = sp.Popen(command, stdin=sp.PIPE) + + if self.total_frames > 0 and not self.quiet: + self.progress_display = ProgressDisplay( + range(self.total_frames), + # bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}", + leave=False, + ascii=True if platform.system() == "Windows" else None, + dynamic_ncols=True, + ) + self.set_progress_display_description() + + def has_progress_display(self): + return self.progress_display is not None + + def set_progress_display_description( + self, file: str = "", sub_desc: str = "" + ) -> None: + if self.progress_display is None: + return + + desc_len = self.progress_description_len + if not file: + file = os.path.split(self.get_movie_file_path())[1] + full_desc = f"{file} {sub_desc}" + if len(full_desc) > desc_len: + full_desc = full_desc[: desc_len - 3] + "..." + else: + full_desc += " " * (desc_len - len(full_desc)) + self.progress_display.set_description(full_desc) + + def write_frame(self, frame: Image) -> None: + if self.write_to_movie: + self.writing_process.stdin.write(frame.tobytes('utf-8')) + if self.progress_display is not None: + self.progress_display.update() + + def close_movie_pipe(self) -> None: + self.writing_process.stdin.close() + self.writing_process.wait() + self.writing_process.terminate() + if self.progress_display is not None: + self.progress_display.close() + + if not self.ended_with_interrupt: + shutil.move(self.temp_file_path, self.final_file_path) + else: + self.movie_file_path = self.temp_file_path + + def combine_movie_files(self) -> None: + kwargs = { + "remove_non_integer_files": True, + "extension": self.movie_file_extension, + } + if self.scene.start_at_animation_number is not None: + kwargs["min_index"] = self.scene.start_at_animation_number + if self.scene.end_at_animation_number is not None: + kwargs["max_index"] = self.scene.end_at_animation_number + else: + kwargs["remove_indices_greater_than"] = self.scene.num_plays - 1 + partial_movie_files = get_sorted_integer_files( + self.partial_movie_directory, **kwargs + ) + if len(partial_movie_files) == 0: + log.warning("No animations in this scene") + return + + # Write a file partial_file_list.txt containing all + # partial movie files + file_list = Path(self.partial_movie_directory) / "partial_movie_file_list.txt" + with open(file_list, "w") as fp: + for pf_path in partial_movie_files: + if os.name == "nt": + pf_path = pf_path.replace("\\", "/") + fp.write(f"file '{pf_path}'\n") + + movie_file_path = self.get_movie_file_path() + commands = [ + config.ffmpeg_executable, + "-y", # overwrite output file if it exists + "-f", + "concat", + "-safe", + "0", + "-i", + file_list, + "-loglevel", + "error", + "-c", + "copy", + movie_file_path, + ] + if not self.includes_sound: + commands.insert(-1, "-an") + + combine_process = sp.Popen(commands) + combine_process.wait() + + def add_sound_to_video(self) -> None: + movie_file_path = self.get_movie_file_path() + stem, ext = os.path.splitext(movie_file_path) + sound_file_path = stem + ".wav" + # Makes sure sound file length will match video file + self.add_audio_segment(AudioSegment.silent(0)) + self.audio_segment.export( + sound_file_path, + bitrate="312k", + ) + temp_file_path = stem + "_temp" + ext + commands = [ + config.ffmpeg_executable, + "-i", + movie_file_path, + "-i", + sound_file_path, + "-y", # overwrite output file if it exists + "-c:v", + "copy", + "-c:a", + "aac", + "-b:a", + "320k", + # select video stream from first file + "-map", + "0:v:0", + # select audio stream from second file + "-map", + "1:a:0", + "-loglevel", + "error", + # "-shortest", + temp_file_path, + ] + sp.call(commands) + shutil.move(temp_file_path, movie_file_path) + os.remove(sound_file_path) + + def save_final_image(self, image: Image) -> None: + file_path = self.get_image_file_path() + image.save(file_path) + self.print_file_ready_message(file_path) + + def print_file_ready_message(self, file_path: str) -> None: + if not self.quiet: + log.info(f"File ready at {file_path}") + + def should_open_file(self) -> bool: + return any( + ( + self.show_file_location_upon_completion, + self.open_file_upon_completion, + ) + ) + + def combine_to_section_videos(self) -> None: + """Concatenate partial movie files for each section.""" + + self.finish_last_section() + sections_index: list[dict[str, Any]] = [] + for section in self.sections: + # only if section does want to be saved + if section.video is not None: + logger.info(f"Combining partial files for section '{section.name}'") + self.combine_files( + section.get_clean_partial_movie_files(), + self.sections_output_dir / section.video, + ) + sections_index.append(section.get_dict(self.sections_output_dir)) + with (self.sections_output_dir / f"{self.output_name}.json").open("w") as file: + json.dump(sections_index, file, indent=4) + + def clean_cache(self): + """Will clean the cache by removing the oldest partial_movie_files.""" + cached_partial_movies = [ + (self.partial_movie_directory / file_name) + for file_name in self.partial_movie_directory.iterdir() + if file_name != "partial_movie_file_list.txt" + ] + if len(cached_partial_movies) > config["max_files_cached"]: + number_files_to_delete = ( + len(cached_partial_movies) - config["max_files_cached"] + ) + oldest_files_to_delete = sorted( + cached_partial_movies, + key=lambda path: path.stat().st_atime, + )[:number_files_to_delete] + for file_to_delete in oldest_files_to_delete: + file_to_delete.unlink() + logger.info( + f"The partial movie directory is full (> {config['max_files_cached']} files). Therefore, manim has removed the {number_files_to_delete} oldest file(s)." + " You can change this behaviour by changing max_files_cached in config.", + ) + + def flush_cache_directory(self): + """Delete all the cached partial movie files""" + cached_partial_movies = [ + self.partial_movie_directory / file_name + for file_name in self.partial_movie_directory.iterdir() + if file_name != "partial_movie_file_list.txt" + ] + for f in cached_partial_movies: + f.unlink() + logger.info( + f"Cache flushed. {len(cached_partial_movies)} file(s) deleted in %(par_dir)s.", + {"par_dir": self.partial_movie_directory}, + ) + + def open_file(self) -> None: + if self.quiet: + curr_stdout = sys.stdout + sys.stdout = open(os.devnull, "w") + + current_os = platform.system() + file_paths = [] + + if self.save_last_frame: + file_paths.append(self.get_image_file_path()) + if self.write_to_movie: + file_paths.append(self.get_movie_file_path()) + + for file_path in file_paths: + if current_os == "Windows": + os.startfile(file_path) + else: + commands = [] + if current_os == "Linux": + commands.append("xdg-open") + elif current_os.startswith("CYGWIN"): + commands.append("cygstart") + else: # Assume macOS + commands.append("open") + + if self.show_file_location_upon_completion: + commands.append("-R") + + commands.append(file_path) + + FNULL = open(os.devnull, "w") + sp.call(commands, stdout=FNULL, stderr=sp.STDOUT) + FNULL.close() + + if self.quiet: + sys.stdout.close() + sys.stdout = curr_stdout diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index b28f37e1fa..0a4df68cde 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -2,6 +2,7 @@ from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING +from PIL import Image import moderngl as gl import numpy as np @@ -10,7 +11,7 @@ import manim.constants as const import manim.utils.color.manim_colors as color from manim._config import config, logger -from manim.camera.camera import OpenGLCameraFrame +from manim.camera.camera import Camera from manim.mobject.types.vectorized_mobject import VMobject from manim.renderer.buffers.buffer import STD140BufferFormat from manim.renderer.opengl_shader_program import load_shader_program_by_folder @@ -298,7 +299,7 @@ def use_window(self): self.output_fbo.release() self.output_fbo = self.ctx.detect_framebuffer() - def init_camera(self, camera: OpenGLCameraFrame): + def init_camera(self, camera: Camera): camera_data = { "frame_shape": (14.2222222221, 8.0), "camera_center": camera.get_center(), @@ -362,6 +363,10 @@ def pre_render(self, camera): def post_render(self): self.ctx.copy_framebuffer(self.output_fbo, self.color_buffer_fbo) + def render(self, *args) -> Image: + self.ctx.clear(0, 0, 1) + return Image.new("RGB", config.frame_size, (0, 0, 255)) + def render_program(self, prog, data, indices=None): vbo = self.ctx.buffer(data.tobytes()) ibo = ( @@ -527,7 +532,7 @@ def read_uniforms(mob: OpenGLVMobject): # def init_frame(self, **config) -> None: -# self.frame = OpenGLCameraFrame(**config) +# self.frame = Camera(**config) # def init_context(self, ctx: moderngl.Context | None = None) -> None: # if ctx is None: diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index a8b762226a..0d72715688 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -47,7 +47,7 @@ def __init__(self, scene: m_scene.Scene, size=config.window_size): super().__init__(size=size) self.scene = scene - self.pressed_keys = set() + self.pressed_keys = {} self.title = self.title = f"Manim Community {__version__} - {str(scene)}" self.size = size @@ -90,8 +90,9 @@ def pixel_coords_to_space_coords( self, px: int, py: int, relative: bool = False ) -> np.ndarray: pw, ph = self.size - fw, fh = self.scene.camera.get_frame_shape() - fc = self.scene.camera.get_frame_center() + # TODO + fw, fh = (config.frame_width, config.frame_height) or self.scene.camera.get_frame_shape() + fc = (config.frame_width, config.frame_height) or self.scene.camera.get_frame_center() if relative: return np.array([px / pw, py / ph, 0]) else: diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py new file mode 100644 index 0000000000..d9644a2d18 --- /dev/null +++ b/manim/renderer/render_manager.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import multiprocessing as mp +import queue # NOTE: Cannot use mp.Queue because of auth keys +import time +from typing import TYPE_CHECKING, Any, Iterable + +from manim import config +from .opengl_renderer import OpenGLRenderer +from .opengl_file_writer import FileWriter + +if TYPE_CHECKING: + from ..scene.scene import SceneState + +__all__ = ("RenderManager",) + + +class RenderManager: + """ + Manage rendering in parallel + """ + + def __init__(self, scene_name: str, **kwargs) -> None: + # renderer + self.renderer = OpenGLRenderer(**kwargs) + self.ctx = mp.get_context('spawn') + + # file writer + self.file_writer = FileWriter(scene_name) # TODO + + def begin(self) -> None: + """Set up processes and manager""" + self.processes: queue.Queue[mp.Process] = queue.Queue() + self.manager = mp.Manager() + self.manager_dict = self.manager.dict() + + def start_dt_calculations(self) -> None: + self.last_t = time.perf_counter() + + def refresh_dt(self) -> float: + dt = time.perf_counter() - self.last_t + self.last_t = time.perf_counter() + return dt + + def get_time_progression(self, run_time: float) -> Iterable[float]: + while (dt := self.refresh_dt()) < run_time: + yield dt + + def render_state(self, state: SceneState, parallel: bool = True) -> None: + """Launch a process (optionally in parallel) + to render a frame + """ + if parallel and config.in_parallel: + process = mp.Process( + target=self.render_frame, + args=(state,), + name=str(state.time) + ) + self.processes.put(process) + process.start() + else: + self.render_frame(state) + + # type state: SceneState + def render_frame(self, state: SceneState) -> Any | None: + """Renders a frame based on a state""" + data = self.send_scene_to_renderer(state) + # result = self.file_writer.write(data) + self.manager_dict[state.time] = data + + def send_scene_to_renderer(self, state: SceneState): + """Renders the State""" + result = self.renderer.render(state) + return result + + def get_frames(self) -> list: + """Get a list of every frame produced by the + manager. + + .. warning:: + + This list is _not guarenteed_ to be sorted until + after calling :meth:`.RenderManager.finish` + """ + return self.manager_dict + + def finish(self) -> None: + for process in iter(self.processes.get, None): + process.join() + self.manager_dict = dict(sorted(self.manager_dict.items())) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 96d818da43..1ccc1b7b84 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -14,16 +14,17 @@ from pyglet.window import key from tqdm import tqdm as ProgressDisplay -from manim._config import logger as log +from manim import config, logger from manim.animation.animation import prepare_animation -from manim.camera.camera import OpenGLCamera as Camera +from manim.camera.camera import Camera from manim.constants import DEFAULT_WAIT_TIME from manim.event_handler import EVENT_DISPATCHER from manim.event_handler.event_type import EventType from manim.mobject.frame import FullScreenRectangle -from manim.mobject.mobject import Group, Mobject, Point, _AnimationBuilder +from manim.mobject.mobject import Group, Point, _AnimationBuilder +from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject from manim.mobject.types.vectorized_mobject import VGroup, VMobject -from manim.scene.scene_file_writer import SceneFileWriter +from manim.renderer.render_manager import RenderManager from manim.utils.color import RED from manim.utils.family_ops import ( extract_mobject_family_members, @@ -35,11 +36,10 @@ if TYPE_CHECKING: from typing import Callable, Iterable - from PIL.Image import Image - from manim.animation.animation import Animation -## TODO: these keybindings should be made configureable + +# TODO: these keybindings should be made configureable PAN_3D_KEY = "d" FRAME_SHIFT_KEY = "f" @@ -60,7 +60,6 @@ def __init__( self, window_config: dict = {}, camera_config: dict = {}, - file_writer_config: dict = {}, skip_animations: bool = False, always_update_mobjects: bool = False, start_at_animation_number: int | None = None, @@ -85,10 +84,6 @@ def __init__( self.camera_config = {**self.default_camera_config, **camera_config} self.window_config = {**self.default_window_config, **window_config} - self.file_writer_config = { - **self.default_file_writer_config, - **file_writer_config, - } # Initialize window, if applicable if self.preview: @@ -102,8 +97,7 @@ def __init__( # Core state of the scene self.camera: Camera = Camera(**self.camera_config) - self.file_writer = SceneFileWriter(self, **self.file_writer_config) - self.mobjects: list[Mobject] = [self.camera.frame] + self.mobjects: list[Mobject] = [] self.id_to_mobject_map: dict[int, Mobject] = {} self.num_plays: int = 0 self.time: float = 0 @@ -111,10 +105,13 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] + self.manager = RenderManager( + self.get_default_scene_name() + ) if self.start_at_animation_number is not None: self.skip_animations = True - if self.file_writer.has_progress_display(): + if self.manager.file_writer.has_progress_display(): self.show_animation_progress = False # Items associated with interaction @@ -131,21 +128,33 @@ def __init__( def __str__(self) -> str: return self.__class__.__name__ + def get_default_scene_name(self) -> str: + name = str(self) + saan = self.start_at_animation_number + eaan = self.end_at_animation_number + if saan is not None: + name += f"_{saan}" + if eaan is not None: + name += f"_{eaan}" + return name + def run(self) -> None: + config.scene_name = str(self) self.virtual_animation_start_time: float = 0 self.real_animation_start_time: float = time.time() - self.file_writer.begin() self.setup() try: self.construct() + # wait until all animations rendered in parallel + self.manager.finish() self.interact() except EndScene: pass except KeyboardInterrupt: # Get rid keyboard interrupt symbols print("", end="\r") - self.file_writer.ended_with_interrupt = True + self.manager.file_writer.ended_with_interrupt = True self.tear_down() render = run @@ -165,7 +174,11 @@ def construct(self) -> None: def tear_down(self) -> None: self.stop_skipping() - self.file_writer.finish() + + if config.save_last_frame: + self.update_frame(ignore_skipping=True) + self.manager.file_writer.finish() + if self.window: self.window.destroy() self.window = None @@ -178,15 +191,17 @@ def interact(self) -> None: """ if self.window is None: return - log.info( + logger.info( "\nTips: Using the keys `d`, `f`, or `z` " + "you can interact with the scene. " + "Press `command + q` or `esc` to quit" ) self.skip_animations = False self.refresh_static_mobjects() + self.manager.start_dt_calculations() while not self.is_window_closing(): - self.update_frame(1 / self.camera.fps) + dt = self.manager.refresh_dt() + self.update_frame(dt) def embed( self, @@ -286,14 +301,6 @@ def custom_exc(shell, etype, evalue, tb, tb_offset=None): raise EndScene() # Only these methods should touch the camera - - def get_image(self) -> Image: - return self.camera.get_image() - - def show(self) -> None: - self.update_frame(ignore_skipping=True) - self.get_image().show() - def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None: self.increment_time(dt) self.update_mobjects(dt) @@ -305,8 +312,9 @@ def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None: if self.window: self.window.clear() - self.camera.clear() - self.camera.capture(*self.mobjects) + # self.camera.clear() + state = SceneState(self) + print(self.manager.render_state(state)) if self.window: self.window.swap_buffers() @@ -315,10 +323,6 @@ def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None: if rt < vt: self.update_frame(0) - def emit_frame(self) -> None: - if not self.skip_animations: - self.file_writer.write_frame(self.camera) - # Related to updating def update_mobjects(self, dt: float) -> None: @@ -615,7 +619,7 @@ def get_time_progression( times = np.arange(0, run_time, 1 / self.camera.fps) - self.file_writer.set_progress_display_description(sub_desc=desc) + # self.file_writer.set_progress_display_description(sub_desc=desc) if self.show_animation_progress: return ProgressDisplay( @@ -634,12 +638,13 @@ def get_run_time(self, animations: Iterable[Animation]) -> float: def get_animation_time_progression( self, animations: Iterable[Animation] ) -> list[float] | np.ndarray | ProgressDisplay: + self.manager.start_dt_calculations() animations = list(animations) run_time = self.get_run_time(animations) description = f"{self.num_plays} {animations[0]}" if len(animations) > 1: description += ", etc." - time_progression = self.get_time_progression(run_time, desc=description) + time_progression = self.manager.get_time_progression(run_time) return time_progression def get_wait_time_progression( @@ -657,27 +662,30 @@ def pre_play(self): # Doesn't exist in Main self.update_skipping_status() - if not self.skip_animations: - self.file_writer.begin_animation() + # if not self.skip_animations: + # self.file_writer.begin_animation() if self.window: self.real_animation_start_time = time.time() self.virtual_animation_start_time = self.time self.refresh_static_mobjects() + self.manager.begin() def post_play(self): - if not self.skip_animations: - self.file_writer.end_animation() + # if not self.skip_animations: + # self.manager.file_writer.end_animation() if self.skip_animations and self.window is not None: # Show some quick frames along the way self.update_frame(dt=0, ignore_skipping=True) self.num_plays += 1 + self.manager.finish() def refresh_static_mobjects(self) -> None: - self.camera.refresh_static_mobjects() + # self.camera.refresh_static_mobjects() + ... def begin_animations(self, animations: Iterable[Animation]) -> None: for animation in animations: @@ -719,7 +727,7 @@ def play( lag_ratio: float | None = None, ) -> None: if len(proto_animations) == 0: - log.warning("Called Scene.play with no animations") + logger.warning("Called Scene.play with no animations") return animations = list(map(prepare_animation, proto_animations)) for anim in animations: @@ -745,7 +753,7 @@ def wait( and not ignore_presenter_mode ): if note: - log.info(note) + logger.info(note) self.hold_loop() else: time_progression = self.get_wait_time_progression(duration, stop_condition) @@ -778,6 +786,9 @@ def revert_to_original_skipping_status(self): self.skip_animations = self.original_skipping_status return self + def emit_frame(self) -> None: + pass + def add_sound( self, sound_file: str, @@ -826,6 +837,7 @@ def redo(self): def save_mobject_to_file( self, mobject: Mobject, file_path: str | None = None ) -> None: + return if file_path is None: file_path = self.file_writer.get_saved_mobject_path(mobject) if file_path is None: @@ -855,6 +867,8 @@ def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: if propagate_event is not None and propagate_event is False: return + # TODO + return frame = self.camera.frame # Handle perspective changes if self.window.is_key_pressed(ord(PAN_3D_KEY)): @@ -932,7 +946,7 @@ def on_key_press(self, symbol: int, modifiers: int) -> None: try: char = chr(symbol) except OverflowError: - log.warning("The value of the pressed key is too large.") + logger.warning("The value of the pressed key is too large.") return event_data = {"symbol": symbol, "modifiers": modifiers} @@ -956,7 +970,8 @@ def on_key_press(self, symbol: int, modifiers: int) -> None: self.hold_on_wait = False def on_resize(self, width: int, height: int) -> None: - self.camera.reset_pixel_shape(width, height) + # self.camera.reset_pixel_shape(width, height) + pass def on_show(self) -> None: pass @@ -969,9 +984,14 @@ def on_close(self) -> None: class SceneState: - def __init__(self, scene: Scene, ignore: list[Mobject] | None = None): + def __init__( + self, + scene: Scene, + ignore: list[Mobject] | None = None + ) -> None: self.time = scene.time self.num_plays = scene.num_plays + self.camera = scene.camera.copy() self.mobjects_to_copies = OrderedDict.fromkeys(scene.mobjects) if ignore: for mob in ignore: diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 93185867b2..877d957700 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -274,9 +274,7 @@ def open_movie_pipe(self, file_path: str) -> None: "-vcodec", "prores_ks", ] - elif self.movie_file_extension == ".gif": - command += [] - else: + elif self.movie_file_extension != ".gif": command += [ "-vcodec", "libx264", diff --git a/manim/utils/commands.py b/manim/utils/commands.py index 2ec9a776dd..50a817b5cb 100644 --- a/manim/utils/commands.py +++ b/manim/utils/commands.py @@ -4,7 +4,7 @@ import os from pathlib import Path from subprocess import run -from typing import Generator +from typing import Any, Generator __all__ = [ "capture", @@ -19,7 +19,7 @@ def capture(command, cwd=None, command_input=None): return out, err, p.returncode -def get_video_metadata(path_to_video: str | os.PathLike) -> dict[str]: +def get_video_metadata(path_to_video: str | os.PathLike) -> dict[str, Any]: command = [ "ffprobe", "-v", diff --git a/manim/utils/hashing.py b/manim/utils/hashing.py index 27b182980f..f8a259b06c 100644 --- a/manim/utils/hashing.py +++ b/manim/utils/hashing.py @@ -15,7 +15,7 @@ import numpy as np from manim.animation.animation import Animation -from manim.camera.cairo_camera import CairoCamera as Camera +from manim.camera.camera import Camera from manim.mobject.mobject import Mobject from .. import config, logger From 1d5024a847c14d6d2a9e4c2be130a2aa024df5fb Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Mon, 15 Apr 2024 21:54:58 -0400 Subject: [PATCH 02/36] Add a test scene --- example_scenes/test_new_rendering.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 example_scenes/test_new_rendering.py diff --git a/example_scenes/test_new_rendering.py b/example_scenes/test_new_rendering.py new file mode 100644 index 0000000000..61d740037a --- /dev/null +++ b/example_scenes/test_new_rendering.py @@ -0,0 +1,11 @@ +from manim import * + + +class Test(Scene): + def construct(self) -> None: + c = Circle() + self.play(Create(c)) + + +with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}): + Test().render() From c942de00cdec2a4554ec68c4f796fda6753c2384 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Mon, 15 Apr 2024 22:45:51 -0400 Subject: [PATCH 03/36] Got keys working --- manim/camera/camera.py | 2 ++ manim/renderer/opengl_renderer_window.py | 4 ++-- manim/renderer/render_manager.py | 21 +++++++-------------- manim/scene/scene.py | 16 +++++++--------- 4 files changed, 18 insertions(+), 25 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 47b8aba6de..51cb027e72 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -31,6 +31,8 @@ class Camera(OpenGLMobject): + fps: int = 30 + def __init__( self, frame_shape: tuple[float, float] = (config.frame_width, config.frame_height), diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 0d72715688..63b222b83f 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -47,8 +47,8 @@ def __init__(self, scene: m_scene.Scene, size=config.window_size): super().__init__(size=size) self.scene = scene - self.pressed_keys = {} - self.title = self.title = f"Manim Community {__version__} - {str(scene)}" + self.pressed_keys = set() + self.title = f"Manim Community {__version__} - {scene}" self.size = size mglw.activate_context(window=self) diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index d9644a2d18..b33f488a72 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -2,7 +2,7 @@ import multiprocessing as mp import queue # NOTE: Cannot use mp.Queue because of auth keys -import time +import numpy as np from typing import TYPE_CHECKING, Any, Iterable from manim import config @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ..scene.scene import SceneState + from ..camera.camera import Camera __all__ = ("RenderManager",) @@ -20,12 +21,13 @@ class RenderManager: Manage rendering in parallel """ - def __init__(self, scene_name: str, **kwargs) -> None: + def __init__(self, scene_name: str, camera: Camera, **kwargs) -> None: # renderer self.renderer = OpenGLRenderer(**kwargs) self.ctx = mp.get_context('spawn') # file writer + self.camera = camera self.file_writer = FileWriter(scene_name) # TODO def begin(self) -> None: @@ -34,18 +36,9 @@ def begin(self) -> None: self.manager = mp.Manager() self.manager_dict = self.manager.dict() - def start_dt_calculations(self) -> None: - self.last_t = time.perf_counter() - - def refresh_dt(self) -> float: - dt = time.perf_counter() - self.last_t - self.last_t = time.perf_counter() - return dt - def get_time_progression(self, run_time: float) -> Iterable[float]: - while (dt := self.refresh_dt()) < run_time: - yield dt - + return np.arange(0, run_time, 1 / self.camera.fps) + def render_state(self, state: SceneState, parallel: bool = True) -> None: """Launch a process (optionally in parallel) to render a frame @@ -85,6 +78,6 @@ def get_frames(self) -> list: return self.manager_dict def finish(self) -> None: - for process in iter(self.processes.get, None): + for process in self.processes.queue: process.join() self.manager_dict = dict(sorted(self.manager_dict.items())) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 1ccc1b7b84..d8ab1602c8 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -106,7 +106,8 @@ def __init__( self.undo_stack = [] self.redo_stack = [] self.manager = RenderManager( - self.get_default_scene_name() + self.get_default_scene_name(), + camera=self.camera ) if self.start_at_animation_number is not None: @@ -198,10 +199,8 @@ def interact(self) -> None: ) self.skip_animations = False self.refresh_static_mobjects() - self.manager.start_dt_calculations() while not self.is_window_closing(): - dt = self.manager.refresh_dt() - self.update_frame(dt) + self.update_frame(1 / self.camera.fps) def embed( self, @@ -313,8 +312,8 @@ def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None: if self.window: self.window.clear() # self.camera.clear() - state = SceneState(self) - print(self.manager.render_state(state)) + state = self.get_state() + self.manager.render_state(state) if self.window: self.window.swap_buffers() @@ -638,7 +637,6 @@ def get_run_time(self, animations: Iterable[Animation]) -> float: def get_animation_time_progression( self, animations: Iterable[Animation] ) -> list[float] | np.ndarray | ProgressDisplay: - self.manager.start_dt_calculations() animations = list(animations) run_time = self.get_run_time(animations) description = f"{self.num_plays} {animations[0]}" @@ -962,8 +960,8 @@ def on_key_press(self, symbol: int, modifiers: int) -> None: self.undo() elif char == "z" and modifiers == key.MOD_COMMAND | key.MOD_SHIFT: self.redo() - # command + q - elif char == QUIT_KEY and modifiers == key.MOD_COMMAND: + # command + q or esc + elif (char == QUIT_KEY and modifiers == key.MOD_COMMAND) or char == key.ESCAPE: self.quit_interaction = True # Space or right arrow elif char == " " or symbol == key.RIGHT: From 42484ca1536e358e76320005e0c951b7442f8844 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 08:26:28 -0400 Subject: [PATCH 04/36] yay its a triangle --- manim/renderer/opengl_renderer.py | 45 +++++++++++++++++++++++++------ manim/renderer/render_manager.py | 15 +++-------- manim/scene/scene.py | 2 +- 3 files changed, 42 insertions(+), 20 deletions(-) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 0a4df68cde..3fa55f52ef 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re from functools import lru_cache from pathlib import Path @@ -13,6 +15,7 @@ from manim._config import config, logger from manim.camera.camera import Camera from manim.mobject.types.vectorized_mobject import VMobject +from manim.mobject.geometry.arc import Circle from manim.renderer.buffers.buffer import STD140BufferFormat from manim.renderer.opengl_shader_program import load_shader_program_by_folder from manim.renderer.renderer import ImageType, Renderer, RendererData @@ -311,7 +314,7 @@ def init_camera(self, camera: Camera): } ubo_camera.write(camera_data) - uniforms = dict() + uniforms = {} uniforms["anti_alias_width"] = 0.01977 uniforms["light_source_position"] = (-10, 10, 10) uniforms["pixel_shape"] = (self.pixel_width, self.pixel_height) @@ -363,9 +366,32 @@ def pre_render(self, camera): def post_render(self): self.ctx.copy_framebuffer(self.output_fbo, self.color_buffer_fbo) - def render(self, *args) -> Image: - self.ctx.clear(0, 0, 1) - return Image.new("RGB", config.frame_size, (0, 0, 255)) + def render(self, state) -> Image: + prog = self.ctx.program( + vertex_shader=''' + #version 330 + in vec2 in_vert; + in vec3 in_color; + out vec3 v_color; + void main() { + v_color = in_color; + gl_Position = vec4(in_vert, 0.0, 1.0); + } + ''', + fragment_shader=''' + #version 330 + in vec3 v_color; + out vec3 f_color; + void main() { + f_color = v_color; + } + ''', + ) + verticies = np.array([[1, 1, 0, 0, 1], [0, 0, 0, 0, 1], [-1, 1, 0, 0, 1]]) + vbo = self.ctx.buffer(verticies.astype("f4").tobytes()) + vao = self.ctx.vertex_array(prog, vbo, 'in_vert', 'in_color') + self.ctx.clear() + vao.render(gl.TRIANGLES) def render_program(self, prog, data, indices=None): vbo = self.ctx.buffer(data.tobytes()) @@ -384,10 +410,12 @@ def render_program(self, prog, data, indices=None): ) vao.render(gl.TRIANGLES) + data, data_size = ibo.read(), ibo.size vbo.release() if ibo is not None: ibo.release() vao.release() + return data, data_size def render_vmobject(self, mob: OpenGLVMobject) -> None: # type: ignore self.stencil_buffer_fbo.use() @@ -427,10 +455,11 @@ def enable_depth(mob): # if(mob.has_fill()): # mob.renderer_data.mesh = ... # Triangulation todo - num_mobs = len(mob.family_members_with_points()) + family = mob.family_members_with_points() + num_mobs = len(family) # Another stroke pass is needed in the beginning to deal with transparency properly - for counter, sub in enumerate(mob.family_members_with_points()): + for counter, sub in enumerate(family): if not isinstance(sub.renderer_data, GLRenderData): return enable_depth(sub) @@ -449,7 +478,7 @@ def enable_depth(mob): np.array(range(len(sub.points))), ) - for counter, sub in enumerate(mob.family_members_with_points()): + for counter, sub in enumerate(family): if not isinstance(sub.renderer_data, GLRenderData): return enable_depth(sub) @@ -468,7 +497,7 @@ def enable_depth(mob): sub.renderer_data.vert_indices, ) - for counter, sub in enumerate(mob.family_members_with_points()): + for counter, sub in enumerate(family): if not isinstance(sub.renderer_data, GLRenderData): return enable_depth(sub) diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index b33f488a72..0b4ae48b2b 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -5,7 +5,7 @@ import numpy as np from typing import TYPE_CHECKING, Any, Iterable -from manim import config +from manim import config, logger from .opengl_renderer import OpenGLRenderer from .opengl_file_writer import FileWriter @@ -43,16 +43,9 @@ def render_state(self, state: SceneState, parallel: bool = True) -> None: """Launch a process (optionally in parallel) to render a frame """ - if parallel and config.in_parallel: - process = mp.Process( - target=self.render_frame, - args=(state,), - name=str(state.time) - ) - self.processes.put(process) - process.start() - else: - self.render_frame(state) + if parallel and config.parallel: + logger.warning("Not supported yet") + self.render_frame(state) # type state: SceneState def render_frame(self, state: SceneState) -> Any | None: diff --git a/manim/scene/scene.py b/manim/scene/scene.py index d8ab1602c8..98c09c517b 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -727,7 +727,7 @@ def play( if len(proto_animations) == 0: logger.warning("Called Scene.play with no animations") return - animations = list(map(prepare_animation, proto_animations)) + animations = [prepare_animation(x) for x in proto_animations] for anim in animations: anim.update_rate_info(run_time, rate_func, lag_ratio) self.pre_play() From 8903025e7c026f1244eba1343f0f1b0a669c1092 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 09:48:57 -0400 Subject: [PATCH 05/36] Cursed! --- manim/renderer/opengl_renderer.py | 7 ++++--- manim/scene/scene.py | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 3fa55f52ef..9c1df6c0ab 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -387,10 +387,11 @@ def render(self, state) -> Image: } ''', ) - verticies = np.array([[1, 1, 0, 0, 1], [0, 0, 0, 0, 1], [-1, 1, 0, 0, 1]]) - vbo = self.ctx.buffer(verticies.astype("f4").tobytes()) - vao = self.ctx.vertex_array(prog, vbo, 'in_vert', 'in_color') self.ctx.clear() + for mob in state.mobjects: + verticies = mob.points + vbo = self.ctx.buffer(verticies.astype("f4").tobytes()) + vao = self.ctx.vertex_array(prog, vbo, 'in_vert', 'in_color') vao.render(gl.TRIANGLES) def render_program(self, prog, data, indices=None): diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 98c09c517b..db0b106f24 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -1004,6 +1004,10 @@ def __init__( else: self.mobjects_to_copies[mob] = mob.copy() + @property + def mobjects(self) -> list[Mobject]: + return self.mobjects_to_copies + def __eq__(self, state: SceneState): return all( ( From fb226b0a15816a9c9a73ede04e68ce4991c79e19 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 13:37:13 -0400 Subject: [PATCH 06/36] Allow support for self.add --- manim/renderer/render_manager.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index 0b4ae48b2b..b22247023b 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -26,15 +26,17 @@ def __init__(self, scene_name: str, camera: Camera, **kwargs) -> None: self.renderer = OpenGLRenderer(**kwargs) self.ctx = mp.get_context('spawn') + # setup + self.processes: queue.Queue[mp.Process] = queue.Queue() + self.manager = mp.Manager() + self.manager_dict = self.manager.dict() + # file writer self.camera = camera self.file_writer = FileWriter(scene_name) # TODO def begin(self) -> None: """Set up processes and manager""" - self.processes: queue.Queue[mp.Process] = queue.Queue() - self.manager = mp.Manager() - self.manager_dict = self.manager.dict() def get_time_progression(self, run_time: float) -> Iterable[float]: return np.arange(0, run_time, 1 / self.camera.fps) From dc205bf8a330f3b57444f1cc5a17e840a17462cb Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 14:06:54 -0400 Subject: [PATCH 07/36] Get it working --- example_scenes/new_test_new.py | 4 ++-- manim/renderer/opengl_renderer.py | 29 +---------------------------- manim/renderer/render_manager.py | 3 ++- manim/scene/scene.py | 1 + 4 files changed, 6 insertions(+), 31 deletions(-) diff --git a/example_scenes/new_test_new.py b/example_scenes/new_test_new.py index e196263ecc..bcc66e2a85 100644 --- a/example_scenes/new_test_new.py +++ b/example_scenes/new_test_new.py @@ -12,7 +12,7 @@ from manim.animation.creation import Create, DrawBorderThenFill, Write from manim.animation.fading import FadeIn from manim.animation.transform import Transform -from manim.camera.camera import OpenGLCameraFrame +from manim.camera.camera import Camera from manim.constants import LEFT, OUT, RIGHT, UP from manim.mobject.geometry.arc import Circle from manim.mobject.geometry.polygram import Square @@ -70,7 +70,7 @@ def progress_through_animations(animations): clock_mobject = DecimalNumber(0.0).shift(4 * LEFT + 2.5 * UP) clock_mobject.fix_in_frame() - camera = OpenGLCameraFrame() + camera = Camera() camera.save_state() # renderer.init_camera(camera) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 9c1df6c0ab..43c9dc616b 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -362,38 +362,11 @@ def pre_render(self, camera): self.init_camera(camera=camera) self.render_target_fbo.use() self.render_target_fbo.clear(*self.background_color) + self.ctx.clear() def post_render(self): self.ctx.copy_framebuffer(self.output_fbo, self.color_buffer_fbo) - def render(self, state) -> Image: - prog = self.ctx.program( - vertex_shader=''' - #version 330 - in vec2 in_vert; - in vec3 in_color; - out vec3 v_color; - void main() { - v_color = in_color; - gl_Position = vec4(in_vert, 0.0, 1.0); - } - ''', - fragment_shader=''' - #version 330 - in vec3 v_color; - out vec3 f_color; - void main() { - f_color = v_color; - } - ''', - ) - self.ctx.clear() - for mob in state.mobjects: - verticies = mob.points - vbo = self.ctx.buffer(verticies.astype("f4").tobytes()) - vao = self.ctx.vertex_array(prog, vbo, 'in_vert', 'in_color') - vao.render(gl.TRIANGLES) - def render_program(self, prog, data, indices=None): vbo = self.ctx.buffer(data.tobytes()) ibo = ( diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index b22247023b..df9c82e2a4 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -37,6 +37,7 @@ def __init__(self, scene_name: str, camera: Camera, **kwargs) -> None: def begin(self) -> None: """Set up processes and manager""" + self.renderer.use_window() def get_time_progression(self, run_time: float) -> Iterable[float]: return np.arange(0, run_time, 1 / self.camera.fps) @@ -58,7 +59,7 @@ def render_frame(self, state: SceneState) -> Any | None: def send_scene_to_renderer(self, state: SceneState): """Renders the State""" - result = self.renderer.render(state) + result = self.renderer.render(self.camera, state.mobjects) return result def get_frames(self) -> list: diff --git a/manim/scene/scene.py b/manim/scene/scene.py index db0b106f24..b6caace7fb 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -97,6 +97,7 @@ def __init__( # Core state of the scene self.camera: Camera = Camera(**self.camera_config) + self.camera.save_state() self.mobjects: list[Mobject] = [] self.id_to_mobject_map: dict[int, Mobject] = {} self.num_plays: int = 0 From 6663612fc6d7921138e8c44ec7f3303e60aa9a62 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 14:11:23 -0400 Subject: [PATCH 08/36] change test scene --- example_scenes/test_new_rendering.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/example_scenes/test_new_rendering.py b/example_scenes/test_new_rendering.py index 61d740037a..6baf590149 100644 --- a/example_scenes/test_new_rendering.py +++ b/example_scenes/test_new_rendering.py @@ -3,8 +3,9 @@ class Test(Scene): def construct(self) -> None: - c = Circle() - self.play(Create(c)) + b = ManimBanner() + # self.play(b.expand()) + self.play(DrawBorderThenFill(b)) with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}): From 26dd249f0b5920cd450eac68938ec70a8972e4d6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:14:07 +0000 Subject: [PATCH 09/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/_config/utils.py | 4 ++-- manim/cli/render/global_options.py | 6 +----- manim/renderer/opengl_file_writer.py | 4 ++-- manim/renderer/opengl_renderer.py | 4 ++-- manim/renderer/opengl_renderer_window.py | 10 ++++++++-- manim/renderer/render_manager.py | 14 ++++++++------ manim/scene/scene.py | 11 ++--------- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/manim/_config/utils.py b/manim/_config/utils.py index db1a17093c..86f420eab8 100644 --- a/manim/_config/utils.py +++ b/manim/_config/utils.py @@ -311,7 +311,7 @@ class MyScene(Scene): "write_to_movie", "zero_pad", "force_window", - "parallel" + "parallel", } def __init__(self) -> None: @@ -583,7 +583,7 @@ def digest_parser(self, parser: configparser.ConfigParser) -> ManimConfig: "use_projection_stroke_shaders", "enable_wireframe", "force_window", - "parallel" + "parallel", ]: setattr(self, key, parser["CLI"].getboolean(key, fallback=False)) diff --git a/manim/cli/render/global_options.py b/manim/cli/render/global_options.py index 2ad3c2f28f..6e5bf5e8fc 100644 --- a/manim/cli/render/global_options.py +++ b/manim/cli/render/global_options.py @@ -102,9 +102,5 @@ def validate_gui_location(ctx, param, value): help="Renders animations without outputting image or video files and disables the window", default=False, ), - option( - "--parallel", - default=True, - help="Renders all animations in parallel" - ) + option("--parallel", default=True, help="Renders all animations in parallel"), ) diff --git a/manim/renderer/opengl_file_writer.py b/manim/renderer/opengl_file_writer.py index a1a4de94fd..0529bb6245 100644 --- a/manim/renderer/opengl_file_writer.py +++ b/manim/renderer/opengl_file_writer.py @@ -1,12 +1,12 @@ from __future__ import annotations import os +import pathlib as path import platform import shutil import subprocess as sp import sys from pathlib import Path -import pathlib as path from typing import TYPE_CHECKING import numpy as np @@ -258,7 +258,7 @@ def set_progress_display_description( def write_frame(self, frame: Image) -> None: if self.write_to_movie: - self.writing_process.stdin.write(frame.tobytes('utf-8')) + self.writing_process.stdin.write(frame.tobytes("utf-8")) if self.progress_display is not None: self.progress_display.update() diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 43c9dc616b..53ebc8aa86 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -4,18 +4,18 @@ from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING -from PIL import Image import moderngl as gl import numpy as np +from PIL import Image from typing_extensions import override import manim.constants as const import manim.utils.color.manim_colors as color from manim._config import config, logger from manim.camera.camera import Camera -from manim.mobject.types.vectorized_mobject import VMobject from manim.mobject.geometry.arc import Circle +from manim.mobject.types.vectorized_mobject import VMobject from manim.renderer.buffers.buffer import STD140BufferFormat from manim.renderer.opengl_shader_program import load_shader_program_by_folder from manim.renderer.renderer import ImageType, Renderer, RendererData diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 63b222b83f..73e094b98e 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -91,8 +91,14 @@ def pixel_coords_to_space_coords( ) -> np.ndarray: pw, ph = self.size # TODO - fw, fh = (config.frame_width, config.frame_height) or self.scene.camera.get_frame_shape() - fc = (config.frame_width, config.frame_height) or self.scene.camera.get_frame_center() + fw, fh = ( + config.frame_width, + config.frame_height, + ) or self.scene.camera.get_frame_shape() + fc = ( + config.frame_width, + config.frame_height, + ) or self.scene.camera.get_frame_center() if relative: return np.array([px / pw, py / ph, 0]) else: diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index df9c82e2a4..ab439832f1 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -2,16 +2,18 @@ import multiprocessing as mp import queue # NOTE: Cannot use mp.Queue because of auth keys -import numpy as np from typing import TYPE_CHECKING, Any, Iterable +import numpy as np + from manim import config, logger -from .opengl_renderer import OpenGLRenderer + from .opengl_file_writer import FileWriter +from .opengl_renderer import OpenGLRenderer if TYPE_CHECKING: - from ..scene.scene import SceneState from ..camera.camera import Camera + from ..scene.scene import SceneState __all__ = ("RenderManager",) @@ -24,7 +26,7 @@ class RenderManager: def __init__(self, scene_name: str, camera: Camera, **kwargs) -> None: # renderer self.renderer = OpenGLRenderer(**kwargs) - self.ctx = mp.get_context('spawn') + self.ctx = mp.get_context("spawn") # setup self.processes: queue.Queue[mp.Process] = queue.Queue() @@ -41,7 +43,7 @@ def begin(self) -> None: def get_time_progression(self, run_time: float) -> Iterable[float]: return np.arange(0, run_time, 1 / self.camera.fps) - + def render_state(self, state: SceneState, parallel: bool = True) -> None: """Launch a process (optionally in parallel) to render a frame @@ -67,7 +69,7 @@ def get_frames(self) -> list: manager. .. warning:: - + This list is _not guarenteed_ to be sorted until after calling :meth:`.RenderManager.finish` """ diff --git a/manim/scene/scene.py b/manim/scene/scene.py index b6caace7fb..6c7d7c35f5 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -106,10 +106,7 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] - self.manager = RenderManager( - self.get_default_scene_name(), - camera=self.camera - ) + self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera) if self.start_at_animation_number is not None: self.skip_animations = True @@ -983,11 +980,7 @@ def on_close(self) -> None: class SceneState: - def __init__( - self, - scene: Scene, - ignore: list[Mobject] | None = None - ) -> None: + def __init__(self, scene: Scene, ignore: list[Mobject] | None = None) -> None: self.time = scene.time self.num_plays = scene.num_plays self.camera = scene.camera.copy() From 9903162949b324e27f2ae22b2cc409ae7f3bb49e Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 9 Apr 2024 16:48:21 -0400 Subject: [PATCH 10/36] Separate Animation and Scene using SceneBuffer --- manim/animation/animation.py | 84 +++++++++++++-------------------- manim/animation/protocol.py | 32 +++++++++++++ manim/animation/scene_buffer.py | 55 +++++++++++++++++++++ manim/scene/scene.py | 52 +++++++++++--------- 4 files changed, 149 insertions(+), 74 deletions(-) create mode 100644 manim/animation/protocol.py create mode 100644 manim/animation/scene_buffer.py diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 53f8fea5d4..3d24f520da 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -11,15 +11,17 @@ from ..mobject.mobject import Mobject from ..mobject.opengl import opengl_mobject from ..utils.rate_functions import linear, smooth +from .protocol import AnimationProtocol +from .scene_buffer import SceneBuffer __all__ = ["Animation", "Wait", "override_animation"] from copy import deepcopy -from typing import TYPE_CHECKING, Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Callable, Iterable, Sequence, TypeVar if TYPE_CHECKING: - from manim.scene.scene import Scene + from typing_extensions import Self DEFAULT_ANIMATION_RUN_TIME: float = 1.0 @@ -132,12 +134,12 @@ def __init__( run_time: float = DEFAULT_ANIMATION_RUN_TIME, rate_func: Callable[[float], float] = smooth, reverse_rate_function: bool = False, - name: str = None, - remover: bool = False, # remove a mobject from the screen? + name: str | None = None, + remover: bool = False, # remove a mobject from the screen at end of animation suspend_mobject_updating: bool = True, introducer: bool = False, *, - _on_finish: Callable[[], None] = lambda _: None, + _on_finish: Callable[[SceneBuffer], object] = lambda _: None, **kwargs, ) -> None: self._typecheck_input(mobject) @@ -149,7 +151,8 @@ def __init__( self.introducer: bool = introducer self.suspend_mobject_updating: bool = suspend_mobject_updating self.lag_ratio: float = lag_ratio - self._on_finish: Callable[[Scene], None] = _on_finish + self._on_finish = _on_finish + self.buffer = SceneBuffer() if config["renderer"] == RendererType.OPENGL: self.starting_mobject: OpenGLMobject = OpenGLMobject() self.mobject: OpenGLMobject = ( @@ -213,10 +216,10 @@ def begin(self) -> None: self.mobject.suspend_updating() self.interpolate(0) + if self.is_introducer(): + self.buffer.add(self.mobject) + def finish(self) -> None: - # TODO: begin and finish should require a scene as parameter. - # That way Animation.clean_up_from_screen and Scene.add_mobjects_from_animations - # could be removed as they fulfill basically the same purpose. """Finish the animation. This method gets called when the animation is over. @@ -226,44 +229,20 @@ def finish(self) -> None: if self.suspend_mobject_updating and self.mobject is not None: self.mobject.resume_updating() - def clean_up_from_scene(self, scene: Scene) -> None: - """Clean up the :class:`~.Scene` after finishing the animation. - - This includes to :meth:`~.Scene.remove` the Animation's - :class:`~.Mobject` if the animation is a remover. - - Parameters - ---------- - scene - The scene the animation should be cleaned up from. - """ - self._on_finish(scene) + self._on_finish(self.buffer) if self.is_remover(): - scene.remove(self.mobject) - - def _setup_scene(self, scene: Scene) -> None: - """Setup up the :class:`~.Scene` before starting the animation. - - This includes to :meth:`~.Scene.add` the Animation's - :class:`~.Mobject` if the animation is an introducer. - - Parameters - ---------- - scene - The scene the animation should be cleaned up from. - """ - if scene is None: - return - if ( - self.is_introducer() - and self.mobject not in scene.get_mobject_family_members() - ): - scene.add(self.mobject) + self.buffer.remove(self.mobject) def create_starting_mobject(self) -> Mobject: # Keep track of where the mobject starts return self.mobject.copy() + def get_all_animations(self) -> tuple[Animation, ...]: + """This method is to implement an animation protocol, and + is more useful in places like :class:`.AnimationGroup` + """ + return (self,) + def get_all_mobjects(self) -> Sequence[Mobject]: """Get all mobjects involved in the animation. @@ -305,9 +284,9 @@ 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 - return list(filter(lambda m: m is not self.mobject, self.get_all_mobjects())) + return [m for m in self.get_all_mobjects() if m is not self.mobject] - def copy(self) -> Animation: + def copy(self) -> Self: """Create a copy of the animation. Returns @@ -422,7 +401,7 @@ def get_run_time(self) -> float: def set_rate_func( self, rate_func: Callable[[float], float], - ) -> Animation: + ) -> Self: """Set the rate function of the animation. Parameters @@ -451,7 +430,7 @@ def get_rate_func( """ return self.rate_func - def set_name(self, name: str) -> Animation: + def set_name(self, name: str) -> Self: """Set the name of the animation. Parameters @@ -489,7 +468,9 @@ def is_introducer(self) -> bool: def prepare_animation( - anim: Animation | mobject._AnimationBuilder, + anim: AnimationProtocol + | mobject._AnimationBuilder + | opengl_mobject._AnimationBuilder, ) -> Animation: r"""Returns either an unchanged animation, or the animation built from a passed animation factory. @@ -517,10 +498,7 @@ def prepare_animation( TypeError: Object 42 cannot be converted to an animation """ - if isinstance(anim, mobject._AnimationBuilder): - return anim.build() - - if isinstance(anim, opengl_mobject._AnimationBuilder): + if isinstance(anim, (mobject._AnimationBuilder, opengl_mobject._AnimationBuilder)): return anim.build() if isinstance(anim, Animation): @@ -576,7 +554,7 @@ def begin(self) -> None: def finish(self) -> None: pass - def clean_up_from_scene(self, scene: Scene) -> None: + def clean_up_from_scene(self, scene: SceneBuffer) -> None: pass def update_mobjects(self, dt: float) -> None: @@ -625,7 +603,9 @@ def construct(self): """ - def decorator(func): + _F = TypeVar("_F", bound=Callable) + + def decorator(func: _F) -> _F: func._override_animation = animation_class return func diff --git a/manim/animation/protocol.py b/manim/animation/protocol.py new file mode 100644 index 0000000000..d9117347be --- /dev/null +++ b/manim/animation/protocol.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Protocol, Sequence + +if TYPE_CHECKING: + from .animation import Animation + from .scene_buffer import SceneBuffer + + +__all__ = ("AnimationProtocol",) + + +class AnimationProtocol(Protocol): + buffer: SceneBuffer + + def begin(self) -> None: + ... + + def finish(self) -> None: + ... + + def get_all_animations(self) -> Sequence[Animation]: + ... + + def update_mobjects(self, dt: float) -> None: + ... + + def interpolate(self, alpha: float) -> None: + ... + + def get_run_time(self) -> float: + ... diff --git a/manim/animation/scene_buffer.py b/manim/animation/scene_buffer.py new file mode 100644 index 0000000000..9e99b292eb --- /dev/null +++ b/manim/animation/scene_buffer.py @@ -0,0 +1,55 @@ +from typing import TYPE_CHECKING, final + +if TYPE_CHECKING: + from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject + + +__all__ = ["SceneBuffer"] + + +@final +class SceneBuffer: + """ + A "buffer" between :class:`.Scene` and :class:`.Animation` + + Operations an animation wants to do on :class:`.Scene` should be + done here (eg. :meth:`.Scene.add`, :meth:`.Scene.remove`). The + scene will then apply these changes at specific points (namely + at the beginning and end of animations) + + It is the scenes job to clear the buffer in between the beginning + and end of animations. + """ + + def __init__(self) -> None: + self.to_remove: list[Mobject] = [] + self.to_add: list[Mobject] = [] + self.deferred = False + + def add(self, *mobs: Mobject) -> None: + self._check_deferred() + self.to_add.extend(mobs) + + def remove(self, *mobs: Mobject) -> None: + self._check_deferred() + self.to_remove.extend(mobs) + + def clear(self) -> None: + self.to_remove.clear() + self.to_add.clear() + + def deferred_clear(self) -> None: + """Clear ``self`` on next operation""" + self.deferred = True + + def _check_deferred(self) -> None: + if self.deferred: + self.clear() + self.deferred = False + + def __str__(self) -> str: + to_add = self.to_add + to_remove = self.to_remove + return f"{type(self).__name__}({to_add=}, {to_remove=})" + + __repr__ = __str__ diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 6c7d7c35f5..71c94a6f06 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -26,18 +26,17 @@ from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.renderer.render_manager import RenderManager from manim.utils.color import RED -from manim.utils.family_ops import ( - extract_mobject_family_members, - recursive_mobject_remove, -) +from manim.utils.family_ops import extract_mobject_family_members from manim.utils.iterables import list_difference_update from manim.utils.module_ops import get_module if TYPE_CHECKING: - from typing import Callable, Iterable + from typing import Any, Callable, Iterable - from manim.animation.animation import Animation + from PIL.Image import Image + from manim.animation.protocol import AnimationProtocol as Animation + from manim.animation.scene_buffer import SceneBuffer # TODO: these keybindings should be made configureable @@ -136,6 +135,10 @@ def get_default_scene_name(self) -> str: if eaan is not None: name += f"_{eaan}" return name + + def process_buffer(self, buffer: SceneBuffer) -> None: + self.remove(*buffer.to_remove) + self.add(*buffer.to_add) def run(self) -> None: config.scene_name = str(self) @@ -348,7 +351,7 @@ def has_time_based_updaters(self) -> bool: return any( [ sm.has_time_based_updater() - for mob in self.mobjects() + for mob in self.mobjects for sm in mob.get_family() ] ) @@ -630,7 +633,7 @@ def get_time_progression( return times def get_run_time(self, animations: Iterable[Animation]) -> float: - return np.max([animation.get_run_time() for animation in animations]) + return max([animation.get_run_time() for animation in animations]) def get_animation_time_progression( self, animations: Iterable[Animation] @@ -646,7 +649,7 @@ def get_animation_time_progression( def get_wait_time_progression( self, duration: float, stop_condition: Callable[[], bool] | None = None ) -> list[float] | np.ndarray | ProgressDisplay: - kw = {"desc": f"{self.num_plays} Waiting"} + kw: dict[str, Any] = {"desc": f"{self.num_plays} Waiting"} if stop_condition is not None: kw["n_iterations"] = -1 # So it doesn't show % progress kw["override_skip_animations"] = True @@ -684,15 +687,19 @@ def refresh_static_mobjects(self) -> None: ... def begin_animations(self, animations: Iterable[Animation]) -> None: - for animation in animations: - animation.begin() - # Anything animated that's not already in the - # scene gets added to the scene. Note, for - # animated mobjects that are in the family of - # those on screen, this can result in a restructuring - # of the scene.mobjects list, which is usually desired. - if animation.mobject not in self.mobjects: - self.add(animation.mobject) + for animation_obj in animations: + for animation in animation_obj.get_all_animations(): + animation.begin() + self.process_buffer(animation.buffer) + animation.buffer.clear() + + # Anything animated that's not already in the + # scene gets added to the scene. Note, for + # animated mobjects that are in the family of + # those on screen, this can result in a restructuring + # of the scene.mobjects list, which is usually desired. + if animation.is_introducer and animation.mobject not in self.mobjects: + self.add(animation.mobject) def progress_through_animations(self, animations: Iterable[Animation]) -> None: last_t = 0 @@ -701,7 +708,7 @@ def progress_through_animations(self, animations: Iterable[Animation]) -> None: last_t = t for animation in animations: animation.update_mobjects(dt) - alpha = t / animation.run_time + alpha = t / animation.get_run_time() animation.interpolate(alpha) self.update_frame(dt) self.emit_frame() @@ -709,7 +716,8 @@ def progress_through_animations(self, animations: Iterable[Animation]) -> None: def finish_animations(self, animations: Iterable[Animation]) -> None: for animation in animations: animation.finish() - animation.clean_up_from_scene(self) + self.process_buffer(animation.buffer) + if self.skip_animations: self.update_mobjects(self.get_run_time(animations)) else: @@ -1002,8 +1010,8 @@ def __init__(self, scene: Scene, ignore: list[Mobject] | None = None) -> None: def mobjects(self) -> list[Mobject]: return self.mobjects_to_copies - def __eq__(self, state: SceneState): - return all( + def __eq__(self, state: Any) -> bool: + return isinstance(state, SceneState) and all( ( self.time == state.time, self.num_plays == state.num_plays, From e242b5cb2df6802e9949fa8f075137311ea390a1 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 9 Apr 2024 22:02:08 -0400 Subject: [PATCH 11/36] Update subclasses This is likely to be a very bug-prone commit --- manim/animation/animation.py | 5 +-- manim/animation/changing.py | 14 ++++--- manim/animation/composition.py | 42 +++++++++------------ manim/animation/creation.py | 13 ++++--- manim/animation/fading.py | 12 +++--- manim/animation/indication.py | 21 +++-------- manim/animation/movement.py | 3 +- manim/animation/speedmodifier.py | 12 +++--- manim/animation/transform.py | 17 +++++---- manim/animation/transform_matching_parts.py | 12 +++--- 10 files changed, 71 insertions(+), 80 deletions(-) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 3d24f520da..8ce643e7f4 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -335,7 +335,7 @@ def interpolate_submobject( alpha: float, ) -> Animation: # Typically implemented by subclass - pass + raise NotImplementedError() def get_sub_alpha(self, alpha: float, index: int, num_submobjects: int) -> float: """Get the animation progress of any submobjects subanimation. @@ -554,9 +554,6 @@ def begin(self) -> None: def finish(self) -> None: pass - def clean_up_from_scene(self, scene: SceneBuffer) -> None: - pass - def update_mobjects(self, dt: float) -> None: pass diff --git a/manim/animation/changing.py b/manim/animation/changing.py index 0354a1c002..bdb65a33e8 100644 --- a/manim/animation/changing.py +++ b/manim/animation/changing.py @@ -4,9 +4,11 @@ __all__ = ["AnimatedBoundary", "TracedPath"] -from typing import Callable +from typing import TYPE_CHECKING, Callable + +if TYPE_CHECKING: + import numpy.typing as npt -from manim._config import config from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.utils.color import ( @@ -61,7 +63,7 @@ def __init__( ] self.add(*self.boundary_copies) self.total_time = 0 - self.add_updater(lambda m, dt: self.update_boundary_copies(dt)) + self.add_updater(lambda _, dt: self.update_boundary_copies(dt)) def update_boundary_copies(self, dt): # Not actual time, but something which passes at @@ -143,7 +145,9 @@ def construct(self): def __init__( self, - traced_point_func: Callable, + traced_point_func: Callable[ + [], npt.NDArray[npt.float] + ], # TODO: Replace with Callable[[], Point3D] stroke_width: float = 2, stroke_color: ParsableManimColor | None = WHITE, dissipating_time: float | None = None, @@ -155,7 +159,7 @@ def __init__( self.time = 1 if self.dissipating_time else None self.add_updater(self.update_path) - def update_path(self, mob, dt): + def update_path(self, _mob, dt): new_point = self.traced_point_func() if not self.has_points(): self.start_new_path(new_point) diff --git a/manim/animation/composition.py b/manim/animation/composition.py index cc6fa2ca7e..2b95438edc 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -13,7 +13,6 @@ from ..animation.animation import Animation, prepare_animation from ..constants import RendererType from ..mobject.mobject import Group, Mobject -from ..scene.scene import Scene from ..utils.iterables import remove_list_redundancies from ..utils.rate_functions import linear @@ -21,6 +20,7 @@ from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup from ..mobject.types.vectorized_mobject import VGroup + from .scene_buffer import SceneBuffer __all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"] @@ -80,28 +80,28 @@ def __init__( def get_all_mobjects(self) -> Sequence[Mobject]: return list(self.group) + def get_all_animations(self) -> tuple[Animation, ...]: + return tuple(self.animations) + def begin(self) -> None: + super().begin() if self.suspend_mobject_updating: self.group.suspend_updating() - for anim in self.animations: - anim.begin() - - def _setup_scene(self, scene) -> None: - for anim in self.animations: - anim._setup_scene(scene) def finish(self) -> None: for anim in self.animations: + if self.remover: + anim.remover = self.remover anim.finish() + self.process_subanimation_buffer(anim.buffer) + if self.suspend_mobject_updating: self.group.resume_updating() + self._on_finish(self.buffer) - def clean_up_from_scene(self, scene: Scene) -> None: - self._on_finish(scene) - for anim in self.animations: - if self.remover: - anim.remover = self.remover - anim.clean_up_from_scene(scene) + def process_subanimation_buffer(self, buffer: SceneBuffer) -> None: + self.buffer.add(*buffer.to_add) + self.buffer.remove(*buffer.to_remove) def update_mobjects(self, dt: float) -> None: for anim in self.animations: @@ -198,6 +198,10 @@ def begin(self) -> None: assert len(self.animations) > 0 self.update_active_animation(0) + for anim in self.animations: + if not anim.is_introducer() and anim.mobject is not None: + self.buffer.add(anim.mobject) + def finish(self) -> None: while self.active_animation is not None: self.next_animation() @@ -206,16 +210,6 @@ def update_mobjects(self, dt: float) -> None: if self.active_animation: self.active_animation.update_mobjects(dt) - def _setup_scene(self, scene) -> None: - if scene is None: - return - if self.is_introducer(): - for anim in self.animations: - if not anim.is_introducer() and anim.mobject is not None: - scene.add(anim.mobject) - - self.scene = scene - def update_active_animation(self, index: int) -> None: self.active_index = index if index >= len(self.animations): @@ -224,7 +218,6 @@ def update_active_animation(self, index: int) -> None: self.active_end_time: float | None = None else: self.active_animation = self.animations[index] - self.active_animation._setup_scene(self.scene) self.active_animation.begin() self.active_start_time = self.anims_with_timings[index][1] self.active_end_time = self.anims_with_timings[index][2] @@ -236,6 +229,7 @@ def next_animation(self) -> None: """ if self.active_animation is not None: self.active_animation.finish() + self.process_subanimation_buffer(self.active_animation.buffer) self.update_active_animation(self.active_index + 1) def interpolate(self, alpha: float) -> None: diff --git a/manim/animation/creation.py b/manim/animation/creation.py index a2e7f8a362..f6fd67a264 100644 --- a/manim/animation/creation.py +++ b/manim/animation/creation.py @@ -79,6 +79,8 @@ def construct(self): import numpy as np if TYPE_CHECKING: + from typing_extensions import Self + from manim.mobject.text.text_mobject import Text from manim.mobject.opengl.opengl_surface import OpenGLSurface @@ -92,7 +94,7 @@ def construct(self): from ..mobject.mobject import Group, Mobject from ..mobject.types.vectorized_mobject import VMobject from ..utils.bezier import integer_interpolate -from ..utils.rate_functions import double_smooth, linear, smooth +from ..utils.rate_functions import double_smooth, linear class ShowPartial(Animation): @@ -124,12 +126,13 @@ def interpolate_submobject( submobject: Mobject, starting_submobject: Mobject, alpha: float, - ) -> None: + ) -> Self: submobject.pointwise_become_partial( starting_submobject, *self._get_bounds(alpha) ) + return self - def _get_bounds(self, alpha: float) -> None: + def _get_bounds(self, alpha: float) -> tuple[float, float]: raise NotImplementedError("Please use Create or ShowPassingFlash") @@ -318,10 +321,10 @@ def __init__( vmobject: VMobject | OpenGLVMobject, rate_func: Callable[[float], float] = linear, reverse: bool = False, + run_time: float | None = None, + lag_ratio: float | None = None, **kwargs, ) -> None: - run_time: float | None = kwargs.pop("run_time", None) - lag_ratio: float | None = kwargs.pop("lag_ratio", None) run_time, lag_ratio = self._set_default_config_from_length( vmobject, run_time, diff --git a/manim/animation/fading.py b/manim/animation/fading.py index fdfbfc27e8..4d620b75d7 100644 --- a/manim/animation/fading.py +++ b/manim/animation/fading.py @@ -80,12 +80,12 @@ def __init__( self.scale_factor = scale super().__init__(mobject, **kwargs) - def _create_faded_mobject(self, fadeIn: bool) -> Mobject: + def _create_faded_mobject(self, fade_in: bool) -> Mobject: """Create a faded, shifted and scaled copy of the mobject. Parameters ---------- - fadeIn + fade_in Whether the faded mobject is used to fade in. Returns @@ -95,7 +95,7 @@ def _create_faded_mobject(self, fadeIn: bool) -> Mobject: """ faded_mobject = self.mobject.copy() faded_mobject.fade(1) - direction_modifier = -1 if fadeIn and not self.point_target else 1 + direction_modifier = -1 if fade_in and not self.point_target else 1 faded_mobject.shift(self.shift_vector * direction_modifier) faded_mobject.scale(self.scale_factor) return faded_mobject @@ -191,8 +191,8 @@ def __init__(self, *mobjects: Mobject, **kwargs) -> None: super().__init__(*mobjects, remover=True, **kwargs) def create_target(self): - return self._create_faded_mobject(fadeIn=False) + return self._create_faded_mobject(fade_in=False) - def clean_up_from_scene(self, scene: Scene = None) -> None: - super().clean_up_from_scene(scene) + def begin(self) -> None: + super().begin() self.interpolate(0) diff --git a/manim/animation/indication.py b/manim/animation/indication.py index fa3d801b24..24d356d172 100644 --- a/manim/animation/indication.py +++ b/manim/animation/indication.py @@ -37,7 +37,7 @@ def construct(self): "Wiggle", ] -from typing import Callable, Iterable, Optional, Tuple, Type, Union +from typing import Callable, Iterable, Optional, Type, Union import numpy as np @@ -305,7 +305,7 @@ def __init__(self, mobject: "VMobject", time_width: float = 0.1, **kwargs) -> No self.time_width = time_width super().__init__(mobject, remover=True, introducer=True, **kwargs) - def _get_bounds(self, alpha: float) -> Tuple[float]: + def _get_bounds(self, alpha: float) -> tuple[float, float]: tw = self.time_width upper = interpolate(0, 1 + tw, alpha) lower = upper - tw @@ -313,8 +313,8 @@ def _get_bounds(self, alpha: float) -> Tuple[float]: lower = max(lower, 0) return (lower, upper) - def clean_up_from_scene(self, scene: "Scene") -> None: - super().clean_up_from_scene(scene) + def finish(self) -> None: + super().finish() for submob, start in self.get_all_families_zipped(): submob.pointwise_become_partial(start, 0, 1) @@ -341,16 +341,6 @@ def __init__(self, vmobject, n_segments=10, time_width=0.1, remover=True, **kwar ) -@deprecated( - since="v0.15.0", - until="v0.16.0", - message="Use Create then FadeOut to achieve this effect.", -) -class ShowCreationThenFadeOut(Succession): - def __init__(self, mobject: "Mobject", remover: bool = True, **kwargs) -> None: - super().__init__(Create(mobject), FadeOut(mobject), remover=remover, **kwargs) - - class ApplyWave(Homotopy): """Send a wave through the Mobject distorting it temporarily. @@ -469,7 +459,7 @@ def homotopy( y: float, z: float, t: float, - ) -> Tuple[float, float, float]: + ) -> tuple[float, float, float]: upper = interpolate(0, 1 + time_width, t) lower = upper - time_width relative_x = inverse_interpolate(x_min, x_max, x) @@ -558,6 +548,7 @@ def interpolate_submobject( ) +# TODO: get rid of this if condition madness class Circumscribe(Succession): """Draw a temporary line surrounding the mobject. diff --git a/manim/animation/movement.py b/manim/animation/movement.py index 816f714477..52db2ec85a 100644 --- a/manim/animation/movement.py +++ b/manim/animation/movement.py @@ -18,7 +18,8 @@ from ..utils.rate_functions import linear if TYPE_CHECKING: - from ..mobject.mobject import Mobject, VMobject + from ..mobject.mobject import Mobject + from ..mobject.types.vectorized_mobject import VMobject class Homotopy(Animation): diff --git a/manim/animation/speedmodifier.py b/manim/animation/speedmodifier.py index 8aa71646b2..9a4d0fa340 100644 --- a/manim/animation/speedmodifier.py +++ b/manim/animation/speedmodifier.py @@ -12,7 +12,6 @@ from ..animation.animation import Animation, Wait, prepare_animation from ..animation.composition import AnimationGroup from ..mobject.mobject import Mobject, Updater, _AnimationBuilder -from ..scene.scene import Scene class ChangeSpeed(Animation): @@ -284,9 +283,10 @@ def finish(self) -> None: def begin(self) -> None: self.anim.begin() + self.buffer.add(*self.anim.buffer.to_add) + self.buffer.remove(*self.anim.buffer.to_remove) - def clean_up_from_scene(self, scene: Scene) -> None: - self.anim.clean_up_from_scene(scene) - - def _setup_scene(self, scene) -> None: - self.anim._setup_scene(scene) + def finish(self) -> None: + self.anim.finish() + self.buffer.add(*self.anim.buffer.to_add) + self.buffer.remove(*self.anim.buffer.to_remove) diff --git a/manim/animation/transform.py b/manim/animation/transform.py index 85b1fbca4b..4ea41f6979 100644 --- a/manim/animation/transform.py +++ b/manim/animation/transform.py @@ -205,10 +205,13 @@ def create_target(self) -> Mobject: # in subclasses return self.target_mobject - def clean_up_from_scene(self, scene: Scene) -> None: - super().clean_up_from_scene(scene) + def finish(self) -> None: + super().finish() if self.replace_mobject_with_target_in_scene: - scene.replace(self.mobject, self.target_mobject) + # Ideally this should stay at the same z-level as + # the original mobject, but this is difficult to implement + self.buffer.remove(self.mobject) + self.buffer.add(self.target_mobject) def get_all_mobjects(self) -> Sequence[Mobject]: return [ @@ -871,11 +874,11 @@ def get_all_mobjects(self) -> Sequence[Mobject]: def get_all_families_zipped(self): return Animation.get_all_families_zipped(self) - def clean_up_from_scene(self, scene): - Animation.clean_up_from_scene(self, scene) - scene.remove(self.mobject) + def finish(self): + Animation.finish(self) # TODO: is this really needed over super()? + self.buffer.remove(self.mobject) self.mobject[0].restore() - scene.add(self.to_add_on_completion) + self.buffer.add(self.to_add_on_completion) class FadeTransformPieces(FadeTransform): diff --git a/manim/animation/transform_matching_parts.py b/manim/animation/transform_matching_parts.py index 8c97643860..8e02b0e800 100644 --- a/manim/animation/transform_matching_parts.py +++ b/manim/animation/transform_matching_parts.py @@ -19,9 +19,6 @@ from .fading import FadeIn, FadeOut from .transform import FadeTransformPieces, Transform -if TYPE_CHECKING: - from ..scene.scene import Scene - class TransformMatchingAbstractBase(AnimationGroup): """Abstract base class for transformations that keep track of matching parts. @@ -153,13 +150,14 @@ def get_shape_map(self, mobject: Mobject) -> dict: shape_map[key].add(sm) return shape_map - def clean_up_from_scene(self, scene: Scene) -> None: + def finish(self) -> None: + super().finish() # Interpolate all animations back to 0 to ensure source mobjects remain unchanged. for anim in self.animations: anim.interpolate(0) - scene.remove(self.mobject) - scene.remove(*self.to_remove) - scene.add(self.to_add) + self.buffer.remove(self.mobject) + self.buffer.remove(*self.to_remove) + self.buffer.add(self.to_add) @staticmethod def get_mobject_parts(mobject: Mobject): From ed71db6c2939a7b8d20d251e90e53589378cefa4 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 9 Apr 2024 22:28:06 -0400 Subject: [PATCH 12/36] Fix bugs with not clearing buffer --- manim/animation/animation.py | 10 +++++++++- manim/animation/composition.py | 4 ---- manim/animation/speedmodifier.py | 11 +++-------- manim/scene/scene.py | 1 + 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 8ce643e7f4..88adcfb93f 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -16,7 +16,6 @@ __all__ = ["Animation", "Wait", "override_animation"] - from copy import deepcopy from typing import TYPE_CHECKING, Callable, Iterable, Sequence, TypeVar @@ -273,6 +272,15 @@ def update_mobjects(self, dt: float) -> None: for mob in self.get_all_mobjects_to_update(): mob.update(dt) + def process_subanimation_buffer(self, buffer: SceneBuffer): + """ + This is used in animations that are proxies around + other animations + """ + self.buffer.add(*buffer.to_add) + self.buffer.remove(*buffer.to_remove) + self.buffer.clear() + def get_all_mobjects_to_update(self) -> list[Mobject]: """Get all mobjects to be updated during the animation. diff --git a/manim/animation/composition.py b/manim/animation/composition.py index 2b95438edc..a147782f68 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -99,10 +99,6 @@ def finish(self) -> None: self.group.resume_updating() self._on_finish(self.buffer) - def process_subanimation_buffer(self, buffer: SceneBuffer) -> None: - self.buffer.add(*buffer.to_add) - self.buffer.remove(*buffer.to_remove) - def update_mobjects(self, dt: float) -> None: for anim in self.animations: anim.update_mobjects(dt) diff --git a/manim/animation/speedmodifier.py b/manim/animation/speedmodifier.py index 9a4d0fa340..5f5265ea31 100644 --- a/manim/animation/speedmodifier.py +++ b/manim/animation/speedmodifier.py @@ -277,16 +277,11 @@ def interpolate(self, alpha: float) -> None: def update_mobjects(self, dt: float) -> None: self.anim.update_mobjects(dt) - def finish(self) -> None: - ChangeSpeed.is_changing_dt = False - self.anim.finish() - def begin(self) -> None: self.anim.begin() - self.buffer.add(*self.anim.buffer.to_add) - self.buffer.remove(*self.anim.buffer.to_remove) + self.process_subanimation_buffer(self.anim.buffer) def finish(self) -> None: + ChangeSpeed.is_changing_dt = False self.anim.finish() - self.buffer.add(*self.anim.buffer.to_add) - self.buffer.remove(*self.anim.buffer.to_remove) + self.process_subanimation_buffer(self.anim.buffer) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 71c94a6f06..75f5307b83 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -139,6 +139,7 @@ def get_default_scene_name(self) -> str: def process_buffer(self, buffer: SceneBuffer) -> None: self.remove(*buffer.to_remove) self.add(*buffer.to_add) + buffer.clear() def run(self) -> None: config.scene_name = str(self) From 156712f3186788f11b326cdbbe71cd09646fa7e7 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 9 Apr 2024 22:46:18 -0400 Subject: [PATCH 13/36] remove useless clear in scene.py' --- manim/scene/scene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 75f5307b83..149f8d6a15 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -692,7 +692,6 @@ def begin_animations(self, animations: Iterable[Animation]) -> None: for animation in animation_obj.get_all_animations(): animation.begin() self.process_buffer(animation.buffer) - animation.buffer.clear() # Anything animated that's not already in the # scene gets added to the scene. Note, for From 0cdaf2be0682ccc046c822975d4a0da38f9cf8a1 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 9 Apr 2024 22:49:54 -0400 Subject: [PATCH 14/36] fixes --- example_scenes/new_test_new.py | 3 +++ manim/animation/indication.py | 1 - manim/animation/scene_buffer.py | 5 ++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/example_scenes/new_test_new.py b/example_scenes/new_test_new.py index bcc66e2a85..63589cbe5c 100644 --- a/example_scenes/new_test_new.py +++ b/example_scenes/new_test_new.py @@ -39,6 +39,7 @@ def progress_through_animations(animations): win = Window( width=1920, height=1080, + fullscreen=True, vsync=True, config=Config(double_buffer=True, samples=0), ) @@ -151,6 +152,8 @@ def on_resize(width, height): if not is_finished: if virtual_time >= run_time: animation.finish() + buffer = str(animation.buffer) + print(f"{buffer = }") has_finished = True else: animation.update_mobjects(dt) diff --git a/manim/animation/indication.py b/manim/animation/indication.py index 24d356d172..d042701bcd 100644 --- a/manim/animation/indication.py +++ b/manim/animation/indication.py @@ -31,7 +31,6 @@ def construct(self): "Flash", "ShowPassingFlash", "ShowPassingFlashWithThinningStrokeWidth", - "ShowCreationThenFadeOut", "ApplyWave", "Circumscribe", "Wiggle", diff --git a/manim/animation/scene_buffer.py b/manim/animation/scene_buffer.py index 9e99b292eb..5ea9ad8cfd 100644 --- a/manim/animation/scene_buffer.py +++ b/manim/animation/scene_buffer.py @@ -1,7 +1,6 @@ -from typing import TYPE_CHECKING, final +from typing import final -if TYPE_CHECKING: - from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject __all__ = ["SceneBuffer"] From ae88c732daa2ea5ab385674b5215c83eb0185e99 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 02:50:31 +0000 Subject: [PATCH 15/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/animation/scene_buffer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/animation/scene_buffer.py b/manim/animation/scene_buffer.py index 5ea9ad8cfd..45fc6d8702 100644 --- a/manim/animation/scene_buffer.py +++ b/manim/animation/scene_buffer.py @@ -2,7 +2,6 @@ from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject - __all__ = ["SceneBuffer"] From 96fae7ed7470867ee6fa1c5d0162bcf89c3a0fd9 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sat, 13 Apr 2024 00:43:57 -0400 Subject: [PATCH 16/36] Add Scene.replace functionality --- manim/animation/animation.py | 6 ++++-- manim/animation/scene_buffer.py | 5 +++++ manim/scene/scene.py | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 88adcfb93f..252046e8bc 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -275,10 +275,12 @@ def update_mobjects(self, dt: float) -> None: def process_subanimation_buffer(self, buffer: SceneBuffer): """ This is used in animations that are proxies around - other animations + other animations, like :class:`.AnimationGroup` """ - self.buffer.add(*buffer.to_add) self.buffer.remove(*buffer.to_remove) + for to_replace_pairs in buffer.to_replace: + self.buffer.replace(*to_replace_pairs) + self.buffer.add(*buffer.to_add) self.buffer.clear() def get_all_mobjects_to_update(self) -> list[Mobject]: diff --git a/manim/animation/scene_buffer.py b/manim/animation/scene_buffer.py index 45fc6d8702..3b3747ecbe 100644 --- a/manim/animation/scene_buffer.py +++ b/manim/animation/scene_buffer.py @@ -22,6 +22,7 @@ class SceneBuffer: def __init__(self) -> None: self.to_remove: list[Mobject] = [] self.to_add: list[Mobject] = [] + self.to_replace: list[tuple[Mobject, ...]] = [] self.deferred = False def add(self, *mobs: Mobject) -> None: @@ -32,6 +33,10 @@ def remove(self, *mobs: Mobject) -> None: self._check_deferred() self.to_remove.extend(mobs) + def replace(self, mob: Mobject, *replacements: Mobject) -> None: + self._check_deferred() + self.to_replace.append((mob, *replacements)) + def clear(self) -> None: self.to_remove.clear() self.to_add.clear() diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 149f8d6a15..a602726c9e 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -138,6 +138,8 @@ def get_default_scene_name(self) -> str: def process_buffer(self, buffer: SceneBuffer) -> None: self.remove(*buffer.to_remove) + for to_replace_pairs in buffer.to_replace: + self.replace(*to_replace_pairs) self.add(*buffer.to_add) buffer.clear() From 25fa9f394f50eada1b45a677a0492b5efd50503c Mon Sep 17 00:00:00 2001 From: adeshpande <110117391+JasonGrace2282@users.noreply.github.com> Date: Sat, 13 Apr 2024 00:56:07 -0400 Subject: [PATCH 17/36] Make :class:`.Animation` explicitly implement `AnimationProtocol` --- manim/animation/animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 252046e8bc..82e64c5507 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -27,7 +27,7 @@ DEFAULT_ANIMATION_LAG_RATIO: float = 0.0 -class Animation: +class Animation(AnimationProtocol): """An animation. Animations have a fixed time span. From c8459650f69ff4f653435938cd2f5158fb10432d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 20:11:55 +0000 Subject: [PATCH 18/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/scene/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index a602726c9e..17411b1f2b 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -135,7 +135,7 @@ def get_default_scene_name(self) -> str: if eaan is not None: name += f"_{eaan}" return name - + def process_buffer(self, buffer: SceneBuffer) -> None: self.remove(*buffer.to_remove) for to_replace_pairs in buffer.to_replace: From 133adf7820410c34dfd4f0f9f1305576bb9f8b4f Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 17:38:09 -0400 Subject: [PATCH 19/36] fix a bug with animation restructuring --- manim/animation/animation.py | 10 +++------- manim/animation/composition.py | 10 +++++----- manim/animation/fading.py | 2 +- manim/animation/protocol.py | 3 --- manim/mobject/logo.py | 2 +- manim/scene/scene.py | 16 ++++------------ 6 files changed, 14 insertions(+), 29 deletions(-) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 82e64c5507..8330b07776 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -215,6 +215,8 @@ def begin(self) -> None: self.mobject.suspend_updating() self.interpolate(0) + # TODO: Figure out a way to check + # if self.mobject in scene.get_mobject_family if self.is_introducer(): self.buffer.add(self.mobject) @@ -236,12 +238,6 @@ def create_starting_mobject(self) -> Mobject: # Keep track of where the mobject starts return self.mobject.copy() - def get_all_animations(self) -> tuple[Animation, ...]: - """This method is to implement an animation protocol, and - is more useful in places like :class:`.AnimationGroup` - """ - return (self,) - def get_all_mobjects(self) -> Sequence[Mobject]: """Get all mobjects involved in the animation. @@ -281,7 +277,7 @@ def process_subanimation_buffer(self, buffer: SceneBuffer): for to_replace_pairs in buffer.to_replace: self.buffer.replace(*to_replace_pairs) self.buffer.add(*buffer.to_add) - self.buffer.clear() + buffer.clear() def get_all_mobjects_to_update(self) -> list[Mobject]: """Get all mobjects to be updated during the animation. diff --git a/manim/animation/composition.py b/manim/animation/composition.py index a147782f68..c5ee86c2dc 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -55,7 +55,7 @@ class AnimationGroup(Animation): def __init__( self, *animations: Animation, - group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None, + group: Group | VGroup | OpenGLGroup | OpenGLVGroup | None = None, run_time: float | None = None, rate_func: Callable[[float], float] = linear, lag_ratio: float = 0, @@ -80,11 +80,11 @@ def __init__( def get_all_mobjects(self) -> Sequence[Mobject]: return list(self.group) - def get_all_animations(self) -> tuple[Animation, ...]: - return tuple(self.animations) - def begin(self) -> None: - super().begin() + for anim in self.animations: + anim.begin() + print(anim.buffer) + self.process_subanimation_buffer(anim.buffer) if self.suspend_mobject_updating: self.group.suspend_updating() diff --git a/manim/animation/fading.py b/manim/animation/fading.py index 4d620b75d7..e2e35b6a8b 100644 --- a/manim/animation/fading.py +++ b/manim/animation/fading.py @@ -146,7 +146,7 @@ def create_target(self): return self.mobject def create_starting_mobject(self): - return self._create_faded_mobject(fadeIn=True) + return self._create_faded_mobject(fade_in=True) class FadeOut(_Fade): diff --git a/manim/animation/protocol.py b/manim/animation/protocol.py index d9117347be..3963a49315 100644 --- a/manim/animation/protocol.py +++ b/manim/animation/protocol.py @@ -19,9 +19,6 @@ def begin(self) -> None: def finish(self) -> None: ... - def get_all_animations(self) -> Sequence[Animation]: - ... - def update_mobjects(self, dt: float) -> None: ... diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py index fcb7adef9e..54780f4bbe 100644 --- a/manim/mobject/logo.py +++ b/manim/mobject/logo.py @@ -261,7 +261,7 @@ def construct(self): ) """ - if direction not in ["left", "right", "center"]: + if direction.lower() not in {"left", "right", "center"}: raise ValueError("direction must be 'left', 'right' or 'center'.") m_shape_offset = 6.25 * self.scale_factor diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 17411b1f2b..bc61dacfdc 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -690,18 +690,10 @@ def refresh_static_mobjects(self) -> None: ... def begin_animations(self, animations: Iterable[Animation]) -> None: - for animation_obj in animations: - for animation in animation_obj.get_all_animations(): - animation.begin() - self.process_buffer(animation.buffer) - - # Anything animated that's not already in the - # scene gets added to the scene. Note, for - # animated mobjects that are in the family of - # those on screen, this can result in a restructuring - # of the scene.mobjects list, which is usually desired. - if animation.is_introducer and animation.mobject not in self.mobjects: - self.add(animation.mobject) + for animation in animations: + animation.begin() + print(animation.buffer) + self.process_buffer(animation.buffer) def progress_through_animations(self, animations: Iterable[Animation]) -> None: last_t = 0 From eb320b7715b96b5d8e0a1067e2f9b6327d49c689 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Tue, 16 Apr 2024 22:28:48 -0400 Subject: [PATCH 20/36] fix succession --- example_scenes/test_new_rendering.py | 13 ++++++++++--- manim/animation/animation.py | 5 ++++- manim/animation/composition.py | 8 ++++++-- manim/animation/protocol.py | 1 + manim/mobject/logo.py | 2 +- .../opengl/opengl_vectorized_mobject.py | 14 ++++++++++++++ manim/mobject/types/vectorized_mobject.py | 18 +++++++++++++++++- manim/scene/scene.py | 3 +++ 8 files changed, 56 insertions(+), 8 deletions(-) diff --git a/example_scenes/test_new_rendering.py b/example_scenes/test_new_rendering.py index 6baf590149..2bb3930475 100644 --- a/example_scenes/test_new_rendering.py +++ b/example_scenes/test_new_rendering.py @@ -3,9 +3,16 @@ class Test(Scene): def construct(self) -> None: - b = ManimBanner() - # self.play(b.expand()) - self.play(DrawBorderThenFill(b)) + s = Square() + c = Circle() + st = Star(color=YELLOW, fill_color=YELLOW) + self.play(Succession( + *[ + Create(x) + for x in VGroup(s, c, st).arrange() + ] + )) + with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}): diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 8330b07776..3e453d98e5 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -151,7 +151,10 @@ def __init__( self.suspend_mobject_updating: bool = suspend_mobject_updating self.lag_ratio: float = lag_ratio self._on_finish = _on_finish + self.buffer = SceneBuffer() + self.apply_buffer = False # ask scene to apply buffer + if config["renderer"] == RendererType.OPENGL: self.starting_mobject: OpenGLMobject = OpenGLMobject() self.mobject: OpenGLMobject = ( @@ -328,7 +331,7 @@ def interpolate_mobject(self, alpha: float) -> None: is completed. For example, alpha-values of 0, 0.5, and 1 correspond to the animation being completed 0%, 50%, and 100%, respectively. """ - families = list(self.get_all_families_zipped()) + families = tuple(self.get_all_families_zipped()) for i, mobs in enumerate(families): sub_alpha = self.get_sub_alpha(alpha, i, len(families)) self.interpolate_submobject(*mobs, sub_alpha) diff --git a/manim/animation/composition.py b/manim/animation/composition.py index c5ee86c2dc..27bcac921b 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -9,7 +9,7 @@ from manim.mobject.opengl.opengl_mobject import OpenGLGroup -from .._config import config +from manim import config, logger from ..animation.animation import Animation, prepare_animation from ..constants import RendererType from ..mobject.mobject import Group, Mobject @@ -83,7 +83,6 @@ def get_all_mobjects(self) -> Sequence[Mobject]: def begin(self) -> None: for anim in self.animations: anim.begin() - print(anim.buffer) self.process_subanimation_buffer(anim.buffer) if self.suspend_mobject_updating: self.group.suspend_updating() @@ -125,7 +124,10 @@ def init_run_time(self, run_time) -> float: def build_animations_with_timings(self) -> None: """Creates a list of triplets of the form (anim, start_time, end_time).""" + self.anims_with_timings = [] + """List of tuple[Animation, start_time, end_time]""" + curr_time: float = 0 for anim in self.animations: start_time: float = curr_time @@ -215,6 +217,8 @@ def update_active_animation(self, index: int) -> None: else: self.active_animation = self.animations[index] self.active_animation.begin() + self.process_subanimation_buffer(self.active_animation.buffer) + self.apply_buffer = True self.active_start_time = self.anims_with_timings[index][1] self.active_end_time = self.anims_with_timings[index][2] diff --git a/manim/animation/protocol.py b/manim/animation/protocol.py index 3963a49315..3951efc6e4 100644 --- a/manim/animation/protocol.py +++ b/manim/animation/protocol.py @@ -12,6 +12,7 @@ class AnimationProtocol(Protocol): buffer: SceneBuffer + apply_buffer: bool def begin(self) -> None: ... diff --git a/manim/mobject/logo.py b/manim/mobject/logo.py index 54780f4bbe..4f054c7d72 100644 --- a/manim/mobject/logo.py +++ b/manim/mobject/logo.py @@ -301,7 +301,7 @@ def slide_and_uncover(mob, alpha): if alpha == 1: self.remove(*[self.anim]) self.add_to_back(self.anim) - mob.shapes.set_z_index(0) + mob.shapes.set_z(0) mob.shapes.save_state() mob.M.save_state() diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index 0e501cb87d..fa9aa55000 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -45,6 +45,7 @@ shoelace_direction, z_to_vector, ) +from manim.utils.deprecation import deprecated if TYPE_CHECKING: from typing import Callable, Iterable, Optional, Sequence @@ -1562,6 +1563,19 @@ def __str__(self): f"submobject{'s' if len(self.submobjects) > 0 else ''}" ) + def set_z(self, z: float) -> Self: + self.points[..., -1] = z + return self + + @deprecated( + since="0.18.2", + until="0.19.0", + message="OpenGL has no concept of z_index. Use set_z instead" + ) + def set_z_index(self, z: float) -> Self: + return self.set_z(z) + + def add(self, *vmobjects: OpenGLVMobject): # type: ignore """Checks if all passed elements are an instance of OpenGLVMobject and then add them to submobjects diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index de9e13ac6f..4795a11a67 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -15,7 +15,7 @@ import itertools as it import sys import typing -from typing import Callable, Sequence +from typing import TYPE_CHECKING, Callable, Sequence import numpy as np from PIL.Image import Image @@ -38,9 +38,13 @@ proportions_along_bezier_curve_for_point, ) from ...utils.color import BLACK, WHITE, ManimColor, ParsableManimColor +from ...utils.deprecation import deprecated from ...utils.iterables import make_even, resize_array, stretch_array_to_length, tuplify from ...utils.space_ops import rotate_vector, shoelace_direction +if TYPE_CHECKING: + from typing_extensions import Self + # TODO # - Change cubic curve groups to have 4 points instead of 3 # - Change sub_path idea accordingly @@ -622,6 +626,18 @@ def set_points(self, points): self.points = np.array(points) return self + def set_z(self, z: float) -> Self: + self.points[..., -1] = z + return self + + @deprecated( + since="0.18.2", + until="0.19.0", + message="OpenGL has no concept of z_index. Use set_z instead" + ) + def set_z_index(self, z: float) -> Self: + return self.set_z(z) + def resize_points( self, new_length: int, diff --git a/manim/scene/scene.py b/manim/scene/scene.py index bc61dacfdc..383885d812 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -704,6 +704,9 @@ def progress_through_animations(self, animations: Iterable[Animation]) -> None: animation.update_mobjects(dt) alpha = t / animation.get_run_time() animation.interpolate(alpha) + if animation.apply_buffer: + self.process_buffer(animation.buffer) + animation.apply_buffer = False self.update_frame(dt) self.emit_frame() From a0036ad5e67c7a69936e378d592db3af166d85e5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 02:29:27 +0000 Subject: [PATCH 21/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- example_scenes/test_new_rendering.py | 8 +------- manim/animation/animation.py | 2 +- manim/animation/composition.py | 2 +- manim/mobject/opengl/opengl_vectorized_mobject.py | 5 ++--- manim/mobject/types/vectorized_mobject.py | 2 +- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/example_scenes/test_new_rendering.py b/example_scenes/test_new_rendering.py index 2bb3930475..90b42db035 100644 --- a/example_scenes/test_new_rendering.py +++ b/example_scenes/test_new_rendering.py @@ -6,13 +6,7 @@ def construct(self) -> None: s = Square() c = Circle() st = Star(color=YELLOW, fill_color=YELLOW) - self.play(Succession( - *[ - Create(x) - for x in VGroup(s, c, st).arrange() - ] - )) - + self.play(Succession(*[Create(x) for x in VGroup(s, c, st).arrange()])) with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}): diff --git a/manim/animation/animation.py b/manim/animation/animation.py index 3e453d98e5..af4fe34f5d 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -153,7 +153,7 @@ def __init__( self._on_finish = _on_finish self.buffer = SceneBuffer() - self.apply_buffer = False # ask scene to apply buffer + self.apply_buffer = False # ask scene to apply buffer if config["renderer"] == RendererType.OPENGL: self.starting_mobject: OpenGLMobject = OpenGLMobject() diff --git a/manim/animation/composition.py b/manim/animation/composition.py index 27bcac921b..5467ed2760 100644 --- a/manim/animation/composition.py +++ b/manim/animation/composition.py @@ -7,9 +7,9 @@ import numpy as np +from manim import config, logger from manim.mobject.opengl.opengl_mobject import OpenGLGroup -from manim import config, logger from ..animation.animation import Animation, prepare_animation from ..constants import RendererType from ..mobject.mobject import Group, Mobject diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index fa9aa55000..910e7ff81b 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -29,6 +29,7 @@ quadratic_bezier_remap, ) from manim.utils.color import * +from manim.utils.deprecation import deprecated from manim.utils.iterables import ( listify, make_even, @@ -45,7 +46,6 @@ shoelace_direction, z_to_vector, ) -from manim.utils.deprecation import deprecated if TYPE_CHECKING: from typing import Callable, Iterable, Optional, Sequence @@ -1570,12 +1570,11 @@ def set_z(self, z: float) -> Self: @deprecated( since="0.18.2", until="0.19.0", - message="OpenGL has no concept of z_index. Use set_z instead" + message="OpenGL has no concept of z_index. Use set_z instead", ) def set_z_index(self, z: float) -> Self: return self.set_z(z) - def add(self, *vmobjects: OpenGLVMobject): # type: ignore """Checks if all passed elements are an instance of OpenGLVMobject and then add them to submobjects diff --git a/manim/mobject/types/vectorized_mobject.py b/manim/mobject/types/vectorized_mobject.py index 4795a11a67..6fe51a7c14 100644 --- a/manim/mobject/types/vectorized_mobject.py +++ b/manim/mobject/types/vectorized_mobject.py @@ -633,7 +633,7 @@ def set_z(self, z: float) -> Self: @deprecated( since="0.18.2", until="0.19.0", - message="OpenGL has no concept of z_index. Use set_z instead" + message="OpenGL has no concept of z_index. Use set_z instead", ) def set_z_index(self, z: float) -> Self: return self.set_z(z) From 290e70dd32ff59f6bf6a039b803abfa434ca3b29 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Sun, 28 Apr 2024 10:23:44 -0400 Subject: [PATCH 22/36] Fix typo in render_manager --- manim/renderer/render_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index ab439832f1..bed153dd61 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -48,7 +48,7 @@ def render_state(self, state: SceneState, parallel: bool = True) -> None: """Launch a process (optionally in parallel) to render a frame """ - if parallel and config.parallel: + if parallel and config.in_parallel: logger.warning("Not supported yet") self.render_frame(state) From 7b69e693fe9a06311dad6178ad189b650143e3bf Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 28 Apr 2024 23:21:36 +0200 Subject: [PATCH 23/36] Added window independent resolution rendering --- manim/renderer/opengl_renderer.py | 72 +++++++++++++++---- manim/renderer/opengl_renderer_window.py | 71 ++---------------- .../renderer/shaders/render_texture/frag.glsl | 12 ++++ .../renderer/shaders/render_texture/vert.glsl | 11 +++ manim/scene/scene.py | 29 ++++---- 5 files changed, 98 insertions(+), 97 deletions(-) create mode 100644 manim/renderer/shaders/render_texture/frag.glsl create mode 100644 manim/renderer/shaders/render_texture/vert.glsl diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 53ebc8aa86..eb386ef6d4 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -55,6 +55,10 @@ ("stroke_width", np.float32, (1,)), ("color", np.float32, (4,)), ] +frame_dtype = [ + ("pos", np.float32, (2,)), + ("uv", np.float32, (2,)) +] class GLRenderData(RendererData): @@ -130,11 +134,13 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None): ], ) inner_vert_indices.sort() - rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] + rings = np.arange(1, len(inner_vert_indices) + + 1)[inner_vert_indices % 3 == 2] # Triangulate inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] + inner_tri_indices = inner_vert_indices[earclip_triangulation( + inner_verts, rings)] tri_indices = np.hstack([indices, inner_tri_indices]) self.triangulation = tri_indices @@ -181,7 +187,8 @@ def compute_bounding_box(mob): all_points = np.vstack( [ mob.points, - *(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()), + *(m.get_bounding_box() + for m in mob.get_family()[1:] if m.has_points()), ], ) if len(all_points) == 0: @@ -245,6 +252,9 @@ def __init__( self.stencil_texture = self.ctx.texture( (self.pixel_width, self.pixel_height), components=1, samples=0, dtype="f1" ) + self.render_target_texture = self.ctx.texture( + (self.pixel_width, self.pixel_height), components=4, samples=0, dtype="f1" + ) self.stencil_buffer = self.ctx.renderbuffer( (self.pixel_width, self.pixel_height), components=1, @@ -281,6 +291,11 @@ def __init__( color_attachments=[self.color_buffer] ) + # this is used as destination for copying the rendered target + # and using it as texture on the output_fbo + self.render_target_texture_fbo = self.ctx.framebuffer( + color_attachments=[self.render_target_texture] + ) self.output_fbo = self.ctx.framebuffer( color_attachments=[ self.ctx.renderbuffer( @@ -297,6 +312,9 @@ def __init__( self.vmobject_stroke_program = load_shader_program_by_folder( self.ctx, "quadratic_bezier_stroke" ) + self.render_texture_program = load_shader_program_by_folder( + self.ctx, "render_texture" + ) def use_window(self): self.output_fbo.release() @@ -304,7 +322,7 @@ def use_window(self): def init_camera(self, camera: Camera): camera_data = { - "frame_shape": (14.2222222221, 8.0), + "frame_shape": (16.0, 9.0), "camera_center": camera.get_center(), "camera_rotation": camera.get_inverse_camera_rotation_matrix().T, "focal_distance": camera.get_focal_distance(), @@ -341,7 +359,8 @@ def get_stroke_shader_data(self, mob: OpenGLVMobject) -> np.ndarray: stroke_data["next_point"][:-nppc] = points[nppc:] stroke_data["next_point"][-nppc:] = points[:nppc] stroke_data["color"] = mob.renderer_data.stroke_rgbas - stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape((-1, 1)) + stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape( + (-1, 1)) return stroke_data @@ -360,12 +379,27 @@ def get_fill_shader_data(self, mob: OpenGLVMobject) -> np.ndarray: def pre_render(self, camera): self.init_camera(camera=camera) + self.ctx.clear() self.render_target_fbo.use() self.render_target_fbo.clear(*self.background_color) - self.ctx.clear() def post_render(self): - self.ctx.copy_framebuffer(self.output_fbo, self.color_buffer_fbo) + frame_data = np.zeros(6, dtype=frame_dtype) + frame_data["pos"] = np.array([[-1,-1],[-1,1],[1,-1],[1,-1],[-1,1],[1,1]]) + frame_data["uv"] = np.array([[0,0],[0,1],[1,0],[1,0],[0,1],[1,1]]) + vbo = self.ctx.buffer(frame_data.tobytes()) + format = gl.detect_format(self.render_texture_program, frame_data.dtype.names) + vao = self.ctx.vertex_array( + program=self.render_texture_program, + content=[(vbo, format, *frame_data.dtype.names)] + ) + self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) + self.render_target_texture.use(0) + self.output_fbo.use() + vao.render(gl.TRIANGLES) + vbo.release() + vao.release() + # self.ctx.copy_framebuffer(self.output_fbo, self.color_buffer_fbo) def render_program(self, prog, data, indices=None): vbo = self.ctx.buffer(data.tobytes()) @@ -441,11 +475,13 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs / 2 uniforms["disable_stencil"] = float(True) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer( + self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms( + self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -460,11 +496,13 @@ def enable_depth(mob): # uniforms['z_shift'] = counter/9 uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) - self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer( + self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_fill_program["stencil_texture"] = 0 if sub.has_fill(): - ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) + ProgramManager.write_uniforms( + self.vmobject_fill_program, uniforms) self.render_program( self.vmobject_fill_program, self.get_fill_shader_data(sub), @@ -479,11 +517,13 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer( + self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms( + self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -491,7 +531,8 @@ def enable_depth(mob): ) def get_pixels(self) -> ImageType: - raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats + raw = self.output_fbo.read( + components=4, dtype="f1", clamp=True) # RGBA, floats buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1)) return buf @@ -510,7 +551,8 @@ def init_render_data(mob: OpenGLVMobject): fill_color = np.array([c._internal_value for c in mob.fill_color]) stroke_color = np.array([c._internal_value for c in mob.stroke_color]) mob.renderer_data.fill_rgbas = prepare_array(fill_color, points_length) - mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, points_length) + mob.renderer_data.stroke_rgbas = prepare_array( + stroke_color, points_length) mob.renderer_data.stroke_widths = prepare_array( np.asarray(listify(mob.stroke_width)), points_length ) diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 73e094b98e..3ea0e3c0b9 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -4,7 +4,7 @@ import moderngl_window as mglw import numpy as np -from moderngl_window.context.pyglet.window import Window as PygletWindow +from moderngl_window.context.pyglet.window import Window as FunWindow from moderngl_window.timers.clock import Timer from screeninfo import get_monitors @@ -14,14 +14,14 @@ import manim.scene as m_scene -class Window(PygletWindow): +class Window(FunWindow): fullscreen: bool = False - resizable: bool = True + resizable: bool = False gl_version: tuple[int, int] = (3, 3) vsync: bool = True cursor: bool = True - def __init__(self, scene: m_scene.Scene, size=config.window_size): + def __init__(self, size=config.window_size): # TODO: remove size argument from window init, # move size computation below to config @@ -46,14 +46,14 @@ def __init__(self, scene: m_scene.Scene, size=config.window_size): size = tuple(size) super().__init__(size=size) - self.scene = scene self.pressed_keys = set() self.title = f"Manim Community {__version__} - {scene}" self.size = size mglw.activate_context(window=self) self.timer = Timer() - self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer) + self.config = mglw.WindowConfig( + ctx=self.ctx, wnd=self, timer=self.timer) self.timer.start() # No idea why, but when self.position is set once @@ -105,62 +105,3 @@ def pixel_coords_to_space_coords( return np.array( [fc[0] + px * fw / pw - fw / 2, fc[1] + py * fh / ph - fh / 2, 0] ) - - def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: - super().on_mouse_motion(x, y, dx, dy) - point = self.pixel_coords_to_space_coords(x, y) - d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) - self.scene.on_mouse_motion(point, d_point) - - def on_mouse_drag( - self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int - ) -> None: - super().on_mouse_drag(x, y, dx, dy, buttons, modifiers) - point = self.pixel_coords_to_space_coords(x, y) - d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) - self.scene.on_mouse_drag(point, d_point, buttons, modifiers) - - def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None: - super().on_mouse_press(x, y, button, mods) - point = self.pixel_coords_to_space_coords(x, y) - self.scene.on_mouse_press(point, button, mods) - - def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None: - super().on_mouse_release(x, y, button, mods) - point = self.pixel_coords_to_space_coords(x, y) - self.scene.on_mouse_release(point, button, mods) - - def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None: - super().on_mouse_scroll(x, y, x_offset, y_offset) - point = self.pixel_coords_to_space_coords(x, y) - offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True) - self.scene.on_mouse_scroll(point, offset) - - def on_key_press(self, symbol: int, modifiers: int) -> None: - self.pressed_keys.add(symbol) # Modifiers? - super().on_key_press(symbol, modifiers) - self.scene.on_key_press(symbol, modifiers) - - def on_key_release(self, symbol: int, modifiers: int) -> None: - self.pressed_keys.difference_update({symbol}) # Modifiers? - super().on_key_release(symbol, modifiers) - self.scene.on_key_release(symbol, modifiers) - - def on_resize(self, width: int, height: int) -> None: - super().on_resize(width, height) - self.scene.on_resize(width, height) - - def on_show(self) -> None: - super().on_show() - self.scene.on_show() - - def on_hide(self) -> None: - super().on_hide() - self.scene.on_hide() - - def on_close(self) -> None: - super().on_close() - self.scene.on_close() - - def is_key_pressed(self, symbol: int) -> bool: - return symbol in self.pressed_keys diff --git a/manim/renderer/shaders/render_texture/frag.glsl b/manim/renderer/shaders/render_texture/frag.glsl new file mode 100644 index 0000000000..68cc0b0bcb --- /dev/null +++ b/manim/renderer/shaders/render_texture/frag.glsl @@ -0,0 +1,12 @@ +#version 330 + +uniform sampler2D tex; +in vec2 f_uv; + +out vec4 frag_color; + +void main() { + frag_color = texture(tex, f_uv); + frag_color.a = 1.0; + frag_color.r = 1.0; +} diff --git a/manim/renderer/shaders/render_texture/vert.glsl b/manim/renderer/shaders/render_texture/vert.glsl new file mode 100644 index 0000000000..89c3184617 --- /dev/null +++ b/manim/renderer/shaders/render_texture/vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +in vec2 pos; +in vec2 uv; + +out vec2 f_uv; + +void main() { + gl_Position = vec4(pos, 0.0, 1.0); + f_uv = uv; +} diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 383885d812..03265c4119 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -81,21 +81,16 @@ def __init__( self.embed_exception_mode = embed_exception_mode self.embed_error_sound = embed_error_sound - self.camera_config = {**self.default_camera_config, **camera_config} - self.window_config = {**self.default_window_config, **window_config} - # Initialize window, if applicable if self.preview: from manim.renderer.opengl_renderer_window import Window - self.window = Window(scene=self, **self.window_config) - self.camera_config["ctx"] = self.window.ctx - self.camera_config["fps"] = 30 # Where's that 30 from? + self.window = Window() else: self.window = None # Core state of the scene - self.camera: Camera = Camera(**self.camera_config) + self.camera: Camera = Camera() self.camera.save_state() self.mobjects: list[Mobject] = [] self.id_to_mobject_map: dict[int, Mobject] = {} @@ -105,7 +100,8 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] - self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera) + self.manager = RenderManager( + self.get_default_scene_name(), camera=self.camera) if self.start_at_animation_number is not None: self.skip_animations = True @@ -185,7 +181,7 @@ def tear_down(self) -> None: self.manager.file_writer.finish() if self.window: - self.window.destroy() + self.window.close() self.window = None def interact(self) -> None: @@ -203,7 +199,7 @@ def interact(self) -> None: ) self.skip_animations = False self.refresh_static_mobjects() - while not self.is_window_closing(): + while not self.window.is_closing: self.update_frame(1 / self.camera.fps) def embed( @@ -310,7 +306,7 @@ def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None: if self.skip_animations and not ignore_skipping: return - if self.is_window_closing(): + if self.window.is_closing: raise EndScene() if self.window: @@ -419,7 +415,8 @@ def remove(self, *mobjects_to_remove: Mobject): # with their children, likewise for all ancestors in the extended family. for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) - self.mobjects = list_difference_update(self.mobjects, mob.get_family()) + self.mobjects = list_difference_update( + self.mobjects, mob.get_family()) return self def replace(self, mobject: Mobject, *replacements: Mobject): @@ -442,7 +439,7 @@ def replace(self, mobject: Mobject, *replacements: Mobject): self.mobjects = [ *self.mobjects[:index], *replacements, - *self.mobjects[index + 1 :], + *self.mobjects[index + 1:], ] return self @@ -757,7 +754,8 @@ def wait( logger.info(note) self.hold_loop() else: - time_progression = self.get_wait_time_progression(duration, stop_condition) + time_progression = self.get_wait_time_progression( + duration, stop_condition) last_t = 0 for t in time_progression: dt = t - last_t @@ -853,9 +851,6 @@ def load_mobject(self, file_name): path = os.path.join(directory, file_name) return Mobject.load(path) - def is_window_closing(self): - return self.window and (self.window.is_closing or self.quit_interaction) - # Event handling def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: From 64521c2255dd327961efede599cd120d1a699c68 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 28 Apr 2024 21:23:09 +0000 Subject: [PATCH 24/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/renderer/opengl_renderer.py | 49 +++++++++--------------- manim/renderer/opengl_renderer_window.py | 3 +- manim/scene/scene.py | 11 ++---- 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index eb386ef6d4..270ac3eea8 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -55,10 +55,7 @@ ("stroke_width", np.float32, (1,)), ("color", np.float32, (4,)), ] -frame_dtype = [ - ("pos", np.float32, (2,)), - ("uv", np.float32, (2,)) -] +frame_dtype = [("pos", np.float32, (2,)), ("uv", np.float32, (2,))] class GLRenderData(RendererData): @@ -134,13 +131,11 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None): ], ) inner_vert_indices.sort() - rings = np.arange(1, len(inner_vert_indices) + - 1)[inner_vert_indices % 3 == 2] + rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] # Triangulate inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[earclip_triangulation( - inner_verts, rings)] + inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] tri_indices = np.hstack([indices, inner_tri_indices]) self.triangulation = tri_indices @@ -187,8 +182,7 @@ def compute_bounding_box(mob): all_points = np.vstack( [ mob.points, - *(m.get_bounding_box() - for m in mob.get_family()[1:] if m.has_points()), + *(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()), ], ) if len(all_points) == 0: @@ -359,8 +353,7 @@ def get_stroke_shader_data(self, mob: OpenGLVMobject) -> np.ndarray: stroke_data["next_point"][:-nppc] = points[nppc:] stroke_data["next_point"][-nppc:] = points[:nppc] stroke_data["color"] = mob.renderer_data.stroke_rgbas - stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape( - (-1, 1)) + stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape((-1, 1)) return stroke_data @@ -385,13 +378,15 @@ def pre_render(self, camera): def post_render(self): frame_data = np.zeros(6, dtype=frame_dtype) - frame_data["pos"] = np.array([[-1,-1],[-1,1],[1,-1],[1,-1],[-1,1],[1,1]]) - frame_data["uv"] = np.array([[0,0],[0,1],[1,0],[1,0],[0,1],[1,1]]) + frame_data["pos"] = np.array( + [[-1, -1], [-1, 1], [1, -1], [1, -1], [-1, 1], [1, 1]] + ) + frame_data["uv"] = np.array([[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) vbo = self.ctx.buffer(frame_data.tobytes()) format = gl.detect_format(self.render_texture_program, frame_data.dtype.names) vao = self.ctx.vertex_array( program=self.render_texture_program, - content=[(vbo, format, *frame_data.dtype.names)] + content=[(vbo, format, *frame_data.dtype.names)], ) self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) self.render_target_texture.use(0) @@ -475,13 +470,11 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs / 2 uniforms["disable_stencil"] = float(True) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms( - self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -496,13 +489,11 @@ def enable_depth(mob): # uniforms['z_shift'] = counter/9 uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_fill_program["stencil_texture"] = 0 if sub.has_fill(): - ProgramManager.write_uniforms( - self.vmobject_fill_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) self.render_program( self.vmobject_fill_program, self.get_fill_shader_data(sub), @@ -517,13 +508,11 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms( - self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -531,8 +520,7 @@ def enable_depth(mob): ) def get_pixels(self) -> ImageType: - raw = self.output_fbo.read( - components=4, dtype="f1", clamp=True) # RGBA, floats + raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1)) return buf @@ -551,8 +539,7 @@ def init_render_data(mob: OpenGLVMobject): fill_color = np.array([c._internal_value for c in mob.fill_color]) stroke_color = np.array([c._internal_value for c in mob.stroke_color]) mob.renderer_data.fill_rgbas = prepare_array(fill_color, points_length) - mob.renderer_data.stroke_rgbas = prepare_array( - stroke_color, points_length) + mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, points_length) mob.renderer_data.stroke_widths = prepare_array( np.asarray(listify(mob.stroke_width)), points_length ) diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 3ea0e3c0b9..d23dade862 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -52,8 +52,7 @@ def __init__(self, size=config.window_size): mglw.activate_context(window=self) self.timer = Timer() - self.config = mglw.WindowConfig( - ctx=self.ctx, wnd=self, timer=self.timer) + self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer) self.timer.start() # No idea why, but when self.position is set once diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 03265c4119..5055b9fb1c 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -100,8 +100,7 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] - self.manager = RenderManager( - self.get_default_scene_name(), camera=self.camera) + self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera) if self.start_at_animation_number is not None: self.skip_animations = True @@ -415,8 +414,7 @@ def remove(self, *mobjects_to_remove: Mobject): # with their children, likewise for all ancestors in the extended family. for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) - self.mobjects = list_difference_update( - self.mobjects, mob.get_family()) + self.mobjects = list_difference_update(self.mobjects, mob.get_family()) return self def replace(self, mobject: Mobject, *replacements: Mobject): @@ -439,7 +437,7 @@ def replace(self, mobject: Mobject, *replacements: Mobject): self.mobjects = [ *self.mobjects[:index], *replacements, - *self.mobjects[index + 1:], + *self.mobjects[index + 1 :], ] return self @@ -754,8 +752,7 @@ def wait( logger.info(note) self.hold_loop() else: - time_progression = self.get_wait_time_progression( - duration, stop_condition) + time_progression = self.get_wait_time_progression(duration, stop_condition) last_t = 0 for t in time_progression: dt = t - last_t From 755428d71ede1117a3b6c97e25f6f639d7b3bac6 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 28 Apr 2024 23:25:00 +0200 Subject: [PATCH 25/36] Whops ? --- manim/renderer/opengl_renderer.py | 49 +++++++------------ manim/renderer/opengl_renderer_window.py | 3 +- .../renderer/shaders/render_texture/frag.glsl | 1 - manim/scene/scene.py | 11 ++--- 4 files changed, 23 insertions(+), 41 deletions(-) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index eb386ef6d4..270ac3eea8 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -55,10 +55,7 @@ ("stroke_width", np.float32, (1,)), ("color", np.float32, (4,)), ] -frame_dtype = [ - ("pos", np.float32, (2,)), - ("uv", np.float32, (2,)) -] +frame_dtype = [("pos", np.float32, (2,)), ("uv", np.float32, (2,))] class GLRenderData(RendererData): @@ -134,13 +131,11 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None): ], ) inner_vert_indices.sort() - rings = np.arange(1, len(inner_vert_indices) + - 1)[inner_vert_indices % 3 == 2] + rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] # Triangulate inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[earclip_triangulation( - inner_verts, rings)] + inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] tri_indices = np.hstack([indices, inner_tri_indices]) self.triangulation = tri_indices @@ -187,8 +182,7 @@ def compute_bounding_box(mob): all_points = np.vstack( [ mob.points, - *(m.get_bounding_box() - for m in mob.get_family()[1:] if m.has_points()), + *(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()), ], ) if len(all_points) == 0: @@ -359,8 +353,7 @@ def get_stroke_shader_data(self, mob: OpenGLVMobject) -> np.ndarray: stroke_data["next_point"][:-nppc] = points[nppc:] stroke_data["next_point"][-nppc:] = points[:nppc] stroke_data["color"] = mob.renderer_data.stroke_rgbas - stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape( - (-1, 1)) + stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape((-1, 1)) return stroke_data @@ -385,13 +378,15 @@ def pre_render(self, camera): def post_render(self): frame_data = np.zeros(6, dtype=frame_dtype) - frame_data["pos"] = np.array([[-1,-1],[-1,1],[1,-1],[1,-1],[-1,1],[1,1]]) - frame_data["uv"] = np.array([[0,0],[0,1],[1,0],[1,0],[0,1],[1,1]]) + frame_data["pos"] = np.array( + [[-1, -1], [-1, 1], [1, -1], [1, -1], [-1, 1], [1, 1]] + ) + frame_data["uv"] = np.array([[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) vbo = self.ctx.buffer(frame_data.tobytes()) format = gl.detect_format(self.render_texture_program, frame_data.dtype.names) vao = self.ctx.vertex_array( program=self.render_texture_program, - content=[(vbo, format, *frame_data.dtype.names)] + content=[(vbo, format, *frame_data.dtype.names)], ) self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) self.render_target_texture.use(0) @@ -475,13 +470,11 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs / 2 uniforms["disable_stencil"] = float(True) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms( - self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -496,13 +489,11 @@ def enable_depth(mob): # uniforms['z_shift'] = counter/9 uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_fill_program["stencil_texture"] = 0 if sub.has_fill(): - ProgramManager.write_uniforms( - self.vmobject_fill_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) self.render_program( self.vmobject_fill_program, self.get_fill_shader_data(sub), @@ -517,13 +508,11 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms( - self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -531,8 +520,7 @@ def enable_depth(mob): ) def get_pixels(self) -> ImageType: - raw = self.output_fbo.read( - components=4, dtype="f1", clamp=True) # RGBA, floats + raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1)) return buf @@ -551,8 +539,7 @@ def init_render_data(mob: OpenGLVMobject): fill_color = np.array([c._internal_value for c in mob.fill_color]) stroke_color = np.array([c._internal_value for c in mob.stroke_color]) mob.renderer_data.fill_rgbas = prepare_array(fill_color, points_length) - mob.renderer_data.stroke_rgbas = prepare_array( - stroke_color, points_length) + mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, points_length) mob.renderer_data.stroke_widths = prepare_array( np.asarray(listify(mob.stroke_width)), points_length ) diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index 3ea0e3c0b9..d23dade862 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -52,8 +52,7 @@ def __init__(self, size=config.window_size): mglw.activate_context(window=self) self.timer = Timer() - self.config = mglw.WindowConfig( - ctx=self.ctx, wnd=self, timer=self.timer) + self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer) self.timer.start() # No idea why, but when self.position is set once diff --git a/manim/renderer/shaders/render_texture/frag.glsl b/manim/renderer/shaders/render_texture/frag.glsl index 68cc0b0bcb..9d1097bf2b 100644 --- a/manim/renderer/shaders/render_texture/frag.glsl +++ b/manim/renderer/shaders/render_texture/frag.glsl @@ -8,5 +8,4 @@ out vec4 frag_color; void main() { frag_color = texture(tex, f_uv); frag_color.a = 1.0; - frag_color.r = 1.0; } diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 03265c4119..5055b9fb1c 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -100,8 +100,7 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] - self.manager = RenderManager( - self.get_default_scene_name(), camera=self.camera) + self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera) if self.start_at_animation_number is not None: self.skip_animations = True @@ -415,8 +414,7 @@ def remove(self, *mobjects_to_remove: Mobject): # with their children, likewise for all ancestors in the extended family. for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) - self.mobjects = list_difference_update( - self.mobjects, mob.get_family()) + self.mobjects = list_difference_update(self.mobjects, mob.get_family()) return self def replace(self, mobject: Mobject, *replacements: Mobject): @@ -439,7 +437,7 @@ def replace(self, mobject: Mobject, *replacements: Mobject): self.mobjects = [ *self.mobjects[:index], *replacements, - *self.mobjects[index + 1:], + *self.mobjects[index + 1 :], ] return self @@ -754,8 +752,7 @@ def wait( logger.info(note) self.hold_loop() else: - time_progression = self.get_wait_time_progression( - duration, stop_condition) + time_progression = self.get_wait_time_progression(duration, stop_condition) last_t = 0 for t in time_progression: dt = t - last_t From db3d8ec04301dcb1f11d5b34200d160a73782e24 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 28 Apr 2024 23:28:41 +0200 Subject: [PATCH 26/36] Who needs window names anyway --- manim/renderer/opengl_renderer_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/renderer/opengl_renderer_window.py b/manim/renderer/opengl_renderer_window.py index d23dade862..11c47b6b26 100644 --- a/manim/renderer/opengl_renderer_window.py +++ b/manim/renderer/opengl_renderer_window.py @@ -47,7 +47,7 @@ def __init__(self, size=config.window_size): super().__init__(size=size) self.pressed_keys = set() - self.title = f"Manim Community {__version__} - {scene}" + self.title = f"Manim Community {__version__}" self.size = size mglw.activate_context(window=self) From db5208423b966b0ad54531bee076887c1bcc1f32 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 29 Apr 2024 03:05:10 +0200 Subject: [PATCH 27/36] fixed transform animation --- manim/animation/animation.py | 12 +++------ manim/renderer/opengl_renderer.py | 45 ++++++++++++++++++++----------- manim/renderer/render_manager.py | 3 ++- manim/scene/scene.py | 11 +++++--- 4 files changed, 43 insertions(+), 28 deletions(-) diff --git a/manim/animation/animation.py b/manim/animation/animation.py index af4fe34f5d..7d282350c6 100644 --- a/manim/animation/animation.py +++ b/manim/animation/animation.py @@ -155,14 +155,10 @@ def __init__( self.buffer = SceneBuffer() self.apply_buffer = False # ask scene to apply buffer - if config["renderer"] == RendererType.OPENGL: - self.starting_mobject: OpenGLMobject = OpenGLMobject() - self.mobject: OpenGLMobject = ( - mobject.copy() if mobject is not None else OpenGLMobject() - ) - # else: - # self.starting_mobject: Mobject = Mobject() - # self.mobject: Mobject = mobject if mobject is not None else Mobject() + self.starting_mobject: OpenGLMobject = OpenGLMobject() + self.mobject: OpenGLMobject = ( + mobject if mobject is not None else OpenGLMobject() + ) if kwargs: logger.debug("Animation received extra kwargs: %s", kwargs) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 270ac3eea8..2b0300c8aa 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -131,11 +131,13 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None): ], ) inner_vert_indices.sort() - rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] + rings = np.arange(1, len(inner_vert_indices) + + 1)[inner_vert_indices % 3 == 2] # Triangulate inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] + inner_tri_indices = inner_vert_indices[earclip_triangulation( + inner_verts, rings)] tri_indices = np.hstack([indices, inner_tri_indices]) self.triangulation = tri_indices @@ -182,7 +184,8 @@ def compute_bounding_box(mob): all_points = np.vstack( [ mob.points, - *(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()), + *(m.get_bounding_box() + for m in mob.get_family()[1:] if m.has_points()), ], ) if len(all_points) == 0: @@ -353,7 +356,8 @@ def get_stroke_shader_data(self, mob: OpenGLVMobject) -> np.ndarray: stroke_data["next_point"][:-nppc] = points[nppc:] stroke_data["next_point"][-nppc:] = points[:nppc] stroke_data["color"] = mob.renderer_data.stroke_rgbas - stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape((-1, 1)) + stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape( + (-1, 1)) return stroke_data @@ -381,14 +385,17 @@ def post_render(self): frame_data["pos"] = np.array( [[-1, -1], [-1, 1], [1, -1], [1, -1], [-1, 1], [1, 1]] ) - frame_data["uv"] = np.array([[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) + frame_data["uv"] = np.array( + [[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) vbo = self.ctx.buffer(frame_data.tobytes()) - format = gl.detect_format(self.render_texture_program, frame_data.dtype.names) + format = gl.detect_format( + self.render_texture_program, frame_data.dtype.names) vao = self.ctx.vertex_array( program=self.render_texture_program, content=[(vbo, format, *frame_data.dtype.names)], ) - self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) + self.ctx.copy_framebuffer( + self.render_target_texture_fbo, self.color_buffer_fbo) self.render_target_texture.use(0) self.output_fbo.use() vao.render(gl.TRIANGLES) @@ -470,11 +477,13 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs / 2 uniforms["disable_stencil"] = float(True) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer( + self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms( + self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -489,11 +498,13 @@ def enable_depth(mob): # uniforms['z_shift'] = counter/9 uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) - self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer( + self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_fill_program["stencil_texture"] = 0 if sub.has_fill(): - ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) + ProgramManager.write_uniforms( + self.vmobject_fill_program, uniforms) self.render_program( self.vmobject_fill_program, self.get_fill_shader_data(sub), @@ -508,11 +519,13 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer( + self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms( + self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -520,7 +533,8 @@ def enable_depth(mob): ) def get_pixels(self) -> ImageType: - raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats + raw = self.output_fbo.read( + components=4, dtype="f1", clamp=True) # RGBA, floats buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1)) return buf @@ -539,7 +553,8 @@ def init_render_data(mob: OpenGLVMobject): fill_color = np.array([c._internal_value for c in mob.fill_color]) stroke_color = np.array([c._internal_value for c in mob.stroke_color]) mob.renderer_data.fill_rgbas = prepare_array(fill_color, points_length) - mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, points_length) + mob.renderer_data.stroke_rgbas = prepare_array( + stroke_color, points_length) mob.renderer_data.stroke_widths = prepare_array( np.asarray(listify(mob.stroke_width)), points_length ) diff --git a/manim/renderer/render_manager.py b/manim/renderer/render_manager.py index bed153dd61..233af3585b 100644 --- a/manim/renderer/render_manager.py +++ b/manim/renderer/render_manager.py @@ -36,10 +36,11 @@ def __init__(self, scene_name: str, camera: Camera, **kwargs) -> None: # file writer self.camera = camera self.file_writer = FileWriter(scene_name) # TODO + self.renderer.use_window() def begin(self) -> None: """Set up processes and manager""" - self.renderer.use_window() + ... def get_time_progression(self, run_time: float) -> Iterable[float]: return np.arange(0, run_time, 1 / self.camera.fps) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 5055b9fb1c..03265c4119 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -100,7 +100,8 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] - self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera) + self.manager = RenderManager( + self.get_default_scene_name(), camera=self.camera) if self.start_at_animation_number is not None: self.skip_animations = True @@ -414,7 +415,8 @@ def remove(self, *mobjects_to_remove: Mobject): # with their children, likewise for all ancestors in the extended family. for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) - self.mobjects = list_difference_update(self.mobjects, mob.get_family()) + self.mobjects = list_difference_update( + self.mobjects, mob.get_family()) return self def replace(self, mobject: Mobject, *replacements: Mobject): @@ -437,7 +439,7 @@ def replace(self, mobject: Mobject, *replacements: Mobject): self.mobjects = [ *self.mobjects[:index], *replacements, - *self.mobjects[index + 1 :], + *self.mobjects[index + 1:], ] return self @@ -752,7 +754,8 @@ def wait( logger.info(note) self.hold_loop() else: - time_progression = self.get_wait_time_progression(duration, stop_condition) + time_progression = self.get_wait_time_progression( + duration, stop_condition) last_t = 0 for t in time_progression: dt = t - last_t From 54496324c8c977e4295303c29488a82dee7173d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 01:06:01 +0000 Subject: [PATCH 28/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/renderer/opengl_renderer.py | 45 +++++++++++-------------------- manim/scene/scene.py | 11 +++----- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 2b0300c8aa..270ac3eea8 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -131,13 +131,11 @@ def get_triangulation(self: OpenGLVMobject, normal_vector=None): ], ) inner_vert_indices.sort() - rings = np.arange(1, len(inner_vert_indices) + - 1)[inner_vert_indices % 3 == 2] + rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2] # Triangulate inner_verts = points[inner_vert_indices] - inner_tri_indices = inner_vert_indices[earclip_triangulation( - inner_verts, rings)] + inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)] tri_indices = np.hstack([indices, inner_tri_indices]) self.triangulation = tri_indices @@ -184,8 +182,7 @@ def compute_bounding_box(mob): all_points = np.vstack( [ mob.points, - *(m.get_bounding_box() - for m in mob.get_family()[1:] if m.has_points()), + *(m.get_bounding_box() for m in mob.get_family()[1:] if m.has_points()), ], ) if len(all_points) == 0: @@ -356,8 +353,7 @@ def get_stroke_shader_data(self, mob: OpenGLVMobject) -> np.ndarray: stroke_data["next_point"][:-nppc] = points[nppc:] stroke_data["next_point"][-nppc:] = points[:nppc] stroke_data["color"] = mob.renderer_data.stroke_rgbas - stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape( - (-1, 1)) + stroke_data["stroke_width"] = mob.renderer_data.stroke_widths.reshape((-1, 1)) return stroke_data @@ -385,17 +381,14 @@ def post_render(self): frame_data["pos"] = np.array( [[-1, -1], [-1, 1], [1, -1], [1, -1], [-1, 1], [1, 1]] ) - frame_data["uv"] = np.array( - [[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) + frame_data["uv"] = np.array([[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]]) vbo = self.ctx.buffer(frame_data.tobytes()) - format = gl.detect_format( - self.render_texture_program, frame_data.dtype.names) + format = gl.detect_format(self.render_texture_program, frame_data.dtype.names) vao = self.ctx.vertex_array( program=self.render_texture_program, content=[(vbo, format, *frame_data.dtype.names)], ) - self.ctx.copy_framebuffer( - self.render_target_texture_fbo, self.color_buffer_fbo) + self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo) self.render_target_texture.use(0) self.output_fbo.use() vao.render(gl.TRIANGLES) @@ -477,13 +470,11 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs / 2 uniforms["disable_stencil"] = float(True) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms( - self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -498,13 +489,11 @@ def enable_depth(mob): # uniforms['z_shift'] = counter/9 uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_fill_program["stencil_texture"] = 0 if sub.has_fill(): - ProgramManager.write_uniforms( - self.vmobject_fill_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_fill_program, uniforms) self.render_program( self.vmobject_fill_program, self.get_fill_shader_data(sub), @@ -519,13 +508,11 @@ def enable_depth(mob): uniforms["index"] = (counter + 1) / num_mobs uniforms["disable_stencil"] = float(False) # uniforms['z_shift'] = counter/9 + 1/20 - self.ctx.copy_framebuffer( - self.stencil_texture_fbo, self.stencil_buffer_fbo) + self.ctx.copy_framebuffer(self.stencil_texture_fbo, self.stencil_buffer_fbo) self.stencil_texture.use(0) self.vmobject_stroke_program["stencil_texture"] = 0 if sub.has_stroke(): - ProgramManager.write_uniforms( - self.vmobject_stroke_program, uniforms) + ProgramManager.write_uniforms(self.vmobject_stroke_program, uniforms) self.render_program( self.vmobject_stroke_program, self.get_stroke_shader_data(sub), @@ -533,8 +520,7 @@ def enable_depth(mob): ) def get_pixels(self) -> ImageType: - raw = self.output_fbo.read( - components=4, dtype="f1", clamp=True) # RGBA, floats + raw = self.output_fbo.read(components=4, dtype="f1", clamp=True) # RGBA, floats buf = np.frombuffer(raw, dtype=np.uint8).reshape((1080, 1920, -1)) return buf @@ -553,8 +539,7 @@ def init_render_data(mob: OpenGLVMobject): fill_color = np.array([c._internal_value for c in mob.fill_color]) stroke_color = np.array([c._internal_value for c in mob.stroke_color]) mob.renderer_data.fill_rgbas = prepare_array(fill_color, points_length) - mob.renderer_data.stroke_rgbas = prepare_array( - stroke_color, points_length) + mob.renderer_data.stroke_rgbas = prepare_array(stroke_color, points_length) mob.renderer_data.stroke_widths = prepare_array( np.asarray(listify(mob.stroke_width)), points_length ) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 03265c4119..5055b9fb1c 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -100,8 +100,7 @@ def __init__( self.original_skipping_status: bool = self.skip_animations self.undo_stack = [] self.redo_stack = [] - self.manager = RenderManager( - self.get_default_scene_name(), camera=self.camera) + self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera) if self.start_at_animation_number is not None: self.skip_animations = True @@ -415,8 +414,7 @@ def remove(self, *mobjects_to_remove: Mobject): # with their children, likewise for all ancestors in the extended family. for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) - self.mobjects = list_difference_update( - self.mobjects, mob.get_family()) + self.mobjects = list_difference_update(self.mobjects, mob.get_family()) return self def replace(self, mobject: Mobject, *replacements: Mobject): @@ -439,7 +437,7 @@ def replace(self, mobject: Mobject, *replacements: Mobject): self.mobjects = [ *self.mobjects[:index], *replacements, - *self.mobjects[index + 1:], + *self.mobjects[index + 1 :], ] return self @@ -754,8 +752,7 @@ def wait( logger.info(note) self.hold_loop() else: - time_progression = self.get_wait_time_progression( - duration, stop_condition) + time_progression = self.get_wait_time_progression(duration, stop_condition) last_t = 0 for t in time_progression: dt = t - last_t From 92fec8e78271fa6f354e76a5e7c733c5563184c3 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Mon, 29 Apr 2024 07:36:37 -0400 Subject: [PATCH 29/36] Remove printing of animation buffer --- manim/scene/scene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 5055b9fb1c..991b4626de 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -687,7 +687,6 @@ def refresh_static_mobjects(self) -> None: def begin_animations(self, animations: Iterable[Animation]) -> None: for animation in animations: animation.begin() - print(animation.buffer) self.process_buffer(animation.buffer) def progress_through_animations(self, animations: Iterable[Animation]) -> None: From b9e87af35355d6d9b2cc586bbaa03e23ef8ffe02 Mon Sep 17 00:00:00 2001 From: JasonGrace2282 Date: Mon, 29 Apr 2024 21:34:56 -0400 Subject: [PATCH 30/36] Lint, remove unused imports --- manim/camera/camera.py | 10 ---------- manim/renderer/buffers/buffer.py | 12 ++++++------ manim/renderer/opengl_renderer.py | 19 +++---------------- 3 files changed, 9 insertions(+), 32 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 51cb027e72..90a7d6af95 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -3,18 +3,12 @@ import itertools as it import math import sys -import time from typing import Any, Iterable from manim.renderer.shader_wrapper import ShaderWrapper from ..constants import RADIANS -if sys.version_info < (3, 8): - from backports.cached_property import cached_property -else: - from functools import cached_property - import moderngl import numpy as np from PIL import Image @@ -25,7 +19,6 @@ from manim.utils.color import BLACK, color_to_rgba from ..constants import * -from ..utils.config_ops import _Data from ..utils.simple_functions import fdiv from ..utils.space_ops import normalize @@ -46,9 +39,6 @@ def __init__( self.orientation = Rotation.identity().as_quat() super().__init__(**kwargs) - def init_uniforms(self): - super().init_uniforms() - def init_points(self) -> None: self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) self.set_width(self.frame_shape[0], stretch=True) diff --git a/manim/renderer/buffers/buffer.py b/manim/renderer/buffers/buffer.py index e028ee2654..0c1cd495b9 100644 --- a/manim/renderer/buffers/buffer.py +++ b/manim/renderer/buffers/buffer.py @@ -4,7 +4,7 @@ class STD140BufferFormat: - _GL_DTYPES: dict[str, tuple[str, int, tuple[int, ...]]] = { + _GL_DTYPES: dict[str, tuple[str, type[np.float_], tuple[int, ...]]] = { "int": ("i", np.float32, (1,)), "ivec2": ("i", np.float32, (2,)), "ivec3": ("i", np.float32, (3,)), @@ -44,18 +44,18 @@ class STD140BufferFormat: def __init__( self, name: str, - struct: tuple[(str, str), ...], + struct: tuple[tuple[str, str], ...], ) -> None: self.dtype = [] - self._paddings = dict() # LUT for future writes + self._paddings = {} # LUT for future writes byte_offset = 0 # Track the offset so we can calculate padding for alignment -- NOTE: use RenderDoc to debug for data_type, var_name in struct: - base_char, base_bytesize, shape = self._GL_DTYPES[data_type] + _base_char, base_bytesize, shape = self._GL_DTYPES[data_type] shape = dict(enumerate(shape)) col_len, row_len = shape.get(0, 1), shape.get(1, 1) # Calculate padding for NON (float/vec2) items col_padding = ( - 0 if row_len == 1 and (col_len == 1 or col_len == 2) else 4 - col_len + 0 if row_len == 1 and (col_len in [1, 2]) else 4 - col_len ) # Store padding in LUT self._paddings[var_name] = col_padding @@ -95,7 +95,7 @@ def _write_padded(self, data: tuple | np.ndarray, var: str) -> np.ndarray: try: # This fails for 1D data (python or np.array) return np.pad(data, ((0, 0), (0, self._paddings[var])), mode="constant") - except: + except Exception: return np.pad(data, ((0, self._paddings[var])), mode="constant") def write(self, data: dict) -> None: diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 270ac3eea8..15ed2ac354 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -1,31 +1,18 @@ from __future__ import annotations -import re -from functools import lru_cache -from pathlib import Path -from typing import TYPE_CHECKING - import moderngl as gl import numpy as np -from PIL import Image -from typing_extensions import override +from manim import config, logger import manim.constants as const import manim.utils.color.manim_colors as color -from manim._config import config, logger from manim.camera.camera import Camera -from manim.mobject.geometry.arc import Circle -from manim.mobject.types.vectorized_mobject import VMobject from manim.renderer.buffers.buffer import STD140BufferFormat from manim.renderer.opengl_shader_program import load_shader_program_by_folder from manim.renderer.renderer import ImageType, Renderer, RendererData -from manim.renderer.shader_wrapper import ShaderWrapper -from manim.utils.iterables import listify, resize_array, resize_with_interpolation +from manim.utils.iterables import listify from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector -if TYPE_CHECKING: - from manim.mobject.types.vectorized_mobject import VMobject - import manim.utils.color.core as c from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject @@ -316,7 +303,7 @@ def use_window(self): def init_camera(self, camera: Camera): camera_data = { - "frame_shape": (16.0, 9.0), + "frame_shape": (config.frame_width, config.frame_height), "camera_center": camera.get_center(), "camera_rotation": camera.get_inverse_camera_rotation_matrix().T, "focal_distance": camera.get_focal_distance(), From d5004fe4f97e47ebcdaacde774e17733d59b10e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 01:35:42 +0000 Subject: [PATCH 31/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/camera/camera.py | 6 ++---- manim/renderer/buffers/buffer.py | 4 +--- manim/renderer/opengl_renderer.py | 7 +++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/manim/camera/camera.py b/manim/camera/camera.py index 90a7d6af95..bf6a285caa 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -5,10 +5,6 @@ import sys from typing import Any, Iterable -from manim.renderer.shader_wrapper import ShaderWrapper - -from ..constants import RADIANS - import moderngl import numpy as np from PIL import Image @@ -16,9 +12,11 @@ from manim import config, logger from manim.mobject.opengl.opengl_mobject import OpenGLMobject, OpenGLPoint +from manim.renderer.shader_wrapper import ShaderWrapper from manim.utils.color import BLACK, color_to_rgba from ..constants import * +from ..constants import RADIANS from ..utils.simple_functions import fdiv from ..utils.space_ops import normalize diff --git a/manim/renderer/buffers/buffer.py b/manim/renderer/buffers/buffer.py index 0c1cd495b9..8828b0a5be 100644 --- a/manim/renderer/buffers/buffer.py +++ b/manim/renderer/buffers/buffer.py @@ -54,9 +54,7 @@ def __init__( shape = dict(enumerate(shape)) col_len, row_len = shape.get(0, 1), shape.get(1, 1) # Calculate padding for NON (float/vec2) items - col_padding = ( - 0 if row_len == 1 and (col_len in [1, 2]) else 4 - col_len - ) + col_padding = 0 if row_len == 1 and (col_len in [1, 2]) else 4 - col_len # Store padding in LUT self._paddings[var_name] = col_padding shape = (col_len + col_padding,) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 15ed2ac354..2ed7143b5c 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -3,19 +3,18 @@ import moderngl as gl import numpy as np -from manim import config, logger import manim.constants as const +import manim.utils.color.core as c import manim.utils.color.manim_colors as color +from manim import config, logger from manim.camera.camera import Camera +from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject from manim.renderer.buffers.buffer import STD140BufferFormat from manim.renderer.opengl_shader_program import load_shader_program_by_folder from manim.renderer.renderer import ImageType, Renderer, RendererData from manim.utils.iterables import listify from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector -import manim.utils.color.core as c -from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject - ubo_camera = STD140BufferFormat( "ubo_camera", ( From 5d010d374f47738c8e653e14abab37920885b75b Mon Sep 17 00:00:00 2001 From: Tristan Schulz Date: Tue, 30 Apr 2024 11:17:18 +0200 Subject: [PATCH 32/36] Removed useless reading from the gpu --- manim/renderer/opengl_renderer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manim/renderer/opengl_renderer.py b/manim/renderer/opengl_renderer.py index 270ac3eea8..b6240845e2 100644 --- a/manim/renderer/opengl_renderer.py +++ b/manim/renderer/opengl_renderer.py @@ -314,6 +314,7 @@ def use_window(self): self.output_fbo.release() self.output_fbo = self.ctx.detect_framebuffer() + # TODO this should also be done with the update decorators because if the camera doesn't change this is pretty rough def init_camera(self, camera: Camera): camera_data = { "frame_shape": (16.0, 9.0), @@ -413,12 +414,12 @@ def render_program(self, prog, data, indices=None): ) vao.render(gl.TRIANGLES) - data, data_size = ibo.read(), ibo.size + # data, data_size = ibo.read(), ibo.size vbo.release() if ibo is not None: ibo.release() vao.release() - return data, data_size + # return data, data_size def render_vmobject(self, mob: OpenGLVMobject) -> None: # type: ignore self.stencil_buffer_fbo.use() From 7c7ecab72261ce759fa8237645789b69943cac38 Mon Sep 17 00:00:00 2001 From: Tristan Schulz Date: Tue, 30 Apr 2024 18:50:54 +0200 Subject: [PATCH 33/36] Fixed broken interp function in opengl_vectorized mobject which used non-existing opacity --- manim/mobject/opengl/opengl_vectorized_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index 910e7ff81b..f23c158a25 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -1324,7 +1324,7 @@ def interpolate_color(self, mobject1, mobject2, alpha): attrs = [ "fill_color", "stroke_color", - "opacity", + # "opacity", # TODO: This probably doesn't exist anymore because opacity is now moved into the colors "reflectiveness", "shadow", "gloss", From 97fabab8a0c66fb93ffa51c01c0a134994f7f441 Mon Sep 17 00:00:00 2001 From: Tristan Schulz Date: Wed, 1 May 2024 00:23:12 +0200 Subject: [PATCH 34/36] Fixed TracedPath --- manim/animation/changing.py | 9 ++++++--- manim/mobject/opengl/opengl_vectorized_mobject.py | 10 ++++++++-- manim/scene/scene.py | 1 + 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/manim/animation/changing.py b/manim/animation/changing.py index bdb65a33e8..1d2fe96aac 100644 --- a/manim/animation/changing.py +++ b/manim/animation/changing.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: import numpy.typing as npt +import numpy as np from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.utils.color import ( @@ -147,13 +148,14 @@ def __init__( self, traced_point_func: Callable[ [], npt.NDArray[npt.float] - ], # TODO: Replace with Callable[[], Point3D] + ], # TODO: Replace with Callable[[], Point3D] stroke_width: float = 2, stroke_color: ParsableManimColor | None = WHITE, dissipating_time: float | None = None, + fill_opacity:float = 0, **kwargs, ): - super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, **kwargs) + super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, fill_opacity=fill_opacity, **kwargs) self.traced_point_func = traced_point_func self.dissipating_time = dissipating_time self.time = 1 if self.dissipating_time else None @@ -163,7 +165,8 @@ def update_path(self, _mob, dt): new_point = self.traced_point_func() if not self.has_points(): self.start_new_path(new_point) - self.add_line_to(new_point) + if not np.allclose(self.get_end(), new_point): + self.add_line_to(new_point) if self.dissipating_time: self.time += dt if self.time - 1 > self.dissipating_time: diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index f23c158a25..fe9e94724f 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -5,6 +5,8 @@ from functools import reduce, wraps from typing import TYPE_CHECKING +from numpy.typing import NDArray + import moderngl import numpy as np @@ -498,7 +500,7 @@ def add_quadratic_bezier_curve_to(self, handle, anchor): else: self.append_points([self.get_last_point(), handle, anchor]) - def add_line_to(self, point: Sequence[float]) -> Self: + def add_line_to(self, point: Sequence[float] | NDArray[float]) -> Self: """Add a straight line from the last point of OpenGLVMobject to the given point. Parameters @@ -507,6 +509,10 @@ def add_line_to(self, point: Sequence[float]) -> Self: point end of the straight line. """ + point = np.asarray(point) + if not self.has_points(): + self.points = np.array([point]) + return self end = self.points[-1] alphas = np.linspace(0, 1, self.n_points_per_curve) if self.long_lines: @@ -578,7 +584,7 @@ def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, recurse=True): def add_points_as_corners(self, points): for point in points: self.add_line_to(point) - return points + return self def set_points_as_corners(self, points: Iterable[float]) -> Self: """Given an array of points, set them as corner of the vmobject. diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 991b4626de..cb8e52297f 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -412,6 +412,7 @@ def remove(self, *mobjects_to_remove: Mobject): for mob in mobjects_to_remove: # First restructure self.mobjects so that parents/grandparents/etc. are replaced # with their children, likewise for all ancestors in the extended family. + print(mob) for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) self.mobjects = list_difference_update(self.mobjects, mob.get_family()) From 8c68b4ece58db9446a6323f0f4322cc5f4c9e415 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 22:24:05 +0000 Subject: [PATCH 35/36] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- manim/animation/changing.py | 12 +++++++++--- manim/mobject/opengl/opengl_vectorized_mobject.py | 3 +-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/manim/animation/changing.py b/manim/animation/changing.py index 1d2fe96aac..1a2dcdadc1 100644 --- a/manim/animation/changing.py +++ b/manim/animation/changing.py @@ -10,6 +10,7 @@ import numpy.typing as npt import numpy as np + from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL from manim.mobject.types.vectorized_mobject import VGroup, VMobject from manim.utils.color import ( @@ -148,14 +149,19 @@ def __init__( self, traced_point_func: Callable[ [], npt.NDArray[npt.float] - ], # TODO: Replace with Callable[[], Point3D] + ], # TODO: Replace with Callable[[], Point3D] stroke_width: float = 2, stroke_color: ParsableManimColor | None = WHITE, dissipating_time: float | None = None, - fill_opacity:float = 0, + fill_opacity: float = 0, **kwargs, ): - super().__init__(stroke_color=stroke_color, stroke_width=stroke_width, fill_opacity=fill_opacity, **kwargs) + super().__init__( + stroke_color=stroke_color, + stroke_width=stroke_width, + fill_opacity=fill_opacity, + **kwargs, + ) self.traced_point_func = traced_point_func self.dissipating_time = dissipating_time self.time = 1 if self.dissipating_time else None diff --git a/manim/mobject/opengl/opengl_vectorized_mobject.py b/manim/mobject/opengl/opengl_vectorized_mobject.py index fe9e94724f..f23a3e3caf 100644 --- a/manim/mobject/opengl/opengl_vectorized_mobject.py +++ b/manim/mobject/opengl/opengl_vectorized_mobject.py @@ -5,10 +5,9 @@ from functools import reduce, wraps from typing import TYPE_CHECKING -from numpy.typing import NDArray - import moderngl import numpy as np +from numpy.typing import NDArray from manim import config from manim.constants import * From 27fae834d36e6f3957a60838db6bbf19f3245949 Mon Sep 17 00:00:00 2001 From: Tristan Schulz Date: Wed, 1 May 2024 00:24:38 +0200 Subject: [PATCH 36/36] Whops --- manim/scene/scene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index cb8e52297f..991b4626de 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -412,7 +412,6 @@ def remove(self, *mobjects_to_remove: Mobject): for mob in mobjects_to_remove: # First restructure self.mobjects so that parents/grandparents/etc. are replaced # with their children, likewise for all ancestors in the extended family. - print(mob) for ancestor in mob.get_ancestors(extended=True): self.replace(ancestor, *ancestor.submobjects) self.mobjects = list_difference_update(self.mobjects, mob.get_family())