From 3339aad29ecfa673080bfa07cda383bd5220e4a8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 10:19:44 -0800 Subject: [PATCH 01/37] Separate CameraFrame into its own file --- manimlib/camera/camera.py | 164 +----------------------------- manimlib/camera/camera_frame.py | 173 ++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 162 deletions(-) create mode 100644 manimlib/camera/camera_frame.py diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index b5bbc2cc..5edf5633 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -1,25 +1,20 @@ from __future__ import annotations import itertools as it -import math import moderngl import numpy as np import OpenGL.GL as gl from PIL import Image -from scipy.spatial.transform import Rotation +from manimlib.camera.camera_frame import CameraFrame from manimlib.constants import BLACK -from manimlib.constants import DEGREES, RADIANS from manimlib.constants import DEFAULT_FPS from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH -from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH -from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP +from manimlib.constants import FRAME_WIDTH from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point from manimlib.utils.color import color_to_rgba -from manimlib.utils.simple_functions import fdiv -from manimlib.utils.space_ops import normalize from typing import TYPE_CHECKING @@ -29,161 +24,6 @@ if TYPE_CHECKING: from manimlib.window import Window from typing import Any, Iterable -class CameraFrame(Mobject): - def __init__( - self, - frame_shape: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT), - center_point: Vect3 = ORIGIN, - focal_dist_to_height: float = 2.0, - **kwargs, - ): - self.frame_shape = frame_shape - self.center_point = center_point - self.focal_dist_to_height = focal_dist_to_height - self.view_matrix = np.identity(4) - super().__init__(**kwargs) - - def init_uniforms(self) -> None: - super().init_uniforms() - # As a quaternion - self.uniforms["orientation"] = Rotation.identity().as_quat() - self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height - - def init_points(self) -> None: - self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) - self.set_width(self.frame_shape[0], stretch=True) - self.set_height(self.frame_shape[1], stretch=True) - self.move_to(self.center_point) - - def set_orientation(self, rotation: Rotation): - self.uniforms["orientation"][:] = rotation.as_quat() - return self - - def get_orientation(self): - return Rotation.from_quat(self.uniforms["orientation"]) - - def to_default_state(self): - self.center() - self.set_height(FRAME_HEIGHT) - self.set_width(FRAME_WIDTH) - self.set_orientation(Rotation.identity()) - return self - - def get_euler_angles(self): - return self.get_orientation().as_euler("zxz")[::-1] - - def get_theta(self): - return self.get_euler_angles()[0] - - def get_phi(self): - return self.get_euler_angles()[1] - - def get_gamma(self): - return self.get_euler_angles()[2] - - def get_inverse_camera_rotation_matrix(self): - return self.get_orientation().as_matrix().T - - def get_view_matrix(self): - """ - Returns a 4x4 for the affine transformation mapping a point - into the camera's internal coordinate system - """ - result = self.view_matrix - result[:] = np.identity(4) - result[:3, 3] = -self.get_center() - rotation = np.identity(4) - rotation[:3, :3] = self.get_inverse_camera_rotation_matrix() - result[:] = np.dot(rotation, result) - return result - - def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs): - rot = Rotation.from_rotvec(angle * normalize(axis)) - self.set_orientation(rot * self.get_orientation()) - return self - - def set_euler_angles( - self, - theta: float | None = None, - phi: float | None = None, - gamma: float | None = None, - units: float = RADIANS - ): - eulers = self.get_euler_angles() # theta, phi, gamma - for i, var in enumerate([theta, phi, gamma]): - if var is not None: - eulers[i] = var * units - self.set_orientation(Rotation.from_euler("zxz", eulers[::-1])) - return self - - def reorient( - self, - theta_degrees: float | None = None, - phi_degrees: float | None = None, - gamma_degrees: float | None = None, - ): - """ - Shortcut for set_euler_angles, defaulting to taking - in angles in degrees - """ - self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES) - return self - - def set_theta(self, theta: float): - return self.set_euler_angles(theta=theta) - - def set_phi(self, phi: float): - return self.set_euler_angles(phi=phi) - - def set_gamma(self, gamma: float): - return self.set_euler_angles(gamma=gamma) - - def increment_theta(self, dtheta: float): - self.rotate(dtheta, OUT) - return self - - def increment_phi(self, dphi: float): - self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0]) - return self - - def increment_gamma(self, dgamma: float): - self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2]) - return self - - def set_focal_distance(self, focal_distance: float): - self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height() - return self - - def set_field_of_view(self, field_of_view: float): - self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2) - return self - - def get_shape(self): - return (self.get_width(), self.get_height()) - - def get_center(self) -> np.ndarray: - # Assumes first point is at the center - return self.get_points()[0] - - def get_width(self) -> float: - points = self.get_points() - return points[2, 0] - points[1, 0] - - def get_height(self) -> float: - points = self.get_points() - return points[4, 1] - points[3, 1] - - def get_focal_distance(self) -> float: - return self.uniforms["focal_dist_to_height"] * self.get_height() - - def get_field_of_view(self) -> float: - return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2) - - def get_implied_camera_location(self) -> np.ndarray: - to_camera = self.get_inverse_camera_rotation_matrix()[2] - dist = self.get_focal_distance() - return self.get_center() + dist * to_camera - class Camera(object): def __init__( diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py new file mode 100644 index 00000000..ce0a5846 --- /dev/null +++ b/manimlib/camera/camera_frame.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import math + +import numpy as np +from scipy.spatial.transform import Rotation + +from manimlib.constants import DEGREES, RADIANS +from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH +from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP +from manimlib.mobject.mobject import Mobject +from manimlib.utils.space_ops import normalize + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from manimlib.typing import Vect3 + + +class CameraFrame(Mobject): + def __init__( + self, + frame_shape: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT), + center_point: Vect3 = ORIGIN, + focal_dist_to_height: float = 2.0, + **kwargs, + ): + self.frame_shape = frame_shape + self.center_point = center_point + self.focal_dist_to_height = focal_dist_to_height + self.view_matrix = np.identity(4) + super().__init__(**kwargs) + + def init_uniforms(self) -> None: + super().init_uniforms() + # As a quaternion + self.uniforms["orientation"] = Rotation.identity().as_quat() + self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height + + def init_points(self) -> None: + self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP]) + self.set_width(self.frame_shape[0], stretch=True) + self.set_height(self.frame_shape[1], stretch=True) + self.move_to(self.center_point) + + def set_orientation(self, rotation: Rotation): + self.uniforms["orientation"][:] = rotation.as_quat() + return self + + def get_orientation(self): + return Rotation.from_quat(self.uniforms["orientation"]) + + def to_default_state(self): + self.center() + self.set_height(FRAME_HEIGHT) + self.set_width(FRAME_WIDTH) + self.set_orientation(Rotation.identity()) + return self + + def get_euler_angles(self): + return self.get_orientation().as_euler("zxz")[::-1] + + def get_theta(self): + return self.get_euler_angles()[0] + + def get_phi(self): + return self.get_euler_angles()[1] + + def get_gamma(self): + return self.get_euler_angles()[2] + + def get_inverse_camera_rotation_matrix(self): + return self.get_orientation().as_matrix().T + + def get_view_matrix(self): + """ + Returns a 4x4 for the affine transformation mapping a point + into the camera's internal coordinate system + """ + result = self.view_matrix + result[:] = np.identity(4) + result[:3, 3] = -self.get_center() + rotation = np.identity(4) + rotation[:3, :3] = self.get_inverse_camera_rotation_matrix() + result[:] = np.dot(rotation, result) + return result + + def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs): + rot = Rotation.from_rotvec(angle * normalize(axis)) + self.set_orientation(rot * self.get_orientation()) + return self + + def set_euler_angles( + self, + theta: float | None = None, + phi: float | None = None, + gamma: float | None = None, + units: float = RADIANS + ): + eulers = self.get_euler_angles() # theta, phi, gamma + for i, var in enumerate([theta, phi, gamma]): + if var is not None: + eulers[i] = var * units + self.set_orientation(Rotation.from_euler("zxz", eulers[::-1])) + return self + + def reorient( + self, + theta_degrees: float | None = None, + phi_degrees: float | None = None, + gamma_degrees: float | None = None, + ): + """ + Shortcut for set_euler_angles, defaulting to taking + in angles in degrees + """ + self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES) + return self + + def set_theta(self, theta: float): + return self.set_euler_angles(theta=theta) + + def set_phi(self, phi: float): + return self.set_euler_angles(phi=phi) + + def set_gamma(self, gamma: float): + return self.set_euler_angles(gamma=gamma) + + def increment_theta(self, dtheta: float): + self.rotate(dtheta, OUT) + return self + + def increment_phi(self, dphi: float): + self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0]) + return self + + def increment_gamma(self, dgamma: float): + self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2]) + return self + + def set_focal_distance(self, focal_distance: float): + self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height() + return self + + def set_field_of_view(self, field_of_view: float): + self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2) + return self + + def get_shape(self): + return (self.get_width(), self.get_height()) + + def get_center(self) -> np.ndarray: + # Assumes first point is at the center + return self.get_points()[0] + + def get_width(self) -> float: + points = self.get_points() + return points[2, 0] - points[1, 0] + + def get_height(self) -> float: + points = self.get_points() + return points[4, 1] - points[3, 1] + + def get_focal_distance(self) -> float: + return self.uniforms["focal_dist_to_height"] * self.get_height() + + def get_field_of_view(self) -> float: + return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2) + + def get_implied_camera_location(self) -> np.ndarray: + to_camera = self.get_inverse_camera_rotation_matrix()[2] + dist = self.get_focal_distance() + return self.get_center() + dist * to_camera From 8c1e5f3b42dd187583737939e74402375fc7850a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 10:31:05 -0800 Subject: [PATCH 02/37] Change use_clip_plane to be a function --- manimlib/camera/camera.py | 2 +- manimlib/mobject/types/surface.py | 2 -- manimlib/shader_wrapper.py | 7 +++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 5edf5633..e699acc0 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -294,7 +294,7 @@ class Camera(object): primitive = int(shader_wrapper.render_primitive) self.set_shader_uniforms(shader_program, shader_wrapper) self.set_ctx_depth_test(shader_wrapper.depth_test) - self.set_ctx_clip_plane(shader_wrapper.use_clip_plane) + self.set_ctx_clip_plane(shader_wrapper.use_clip_plane()) if shader_wrapper.is_fill: self.render_fill(render_group["vao"], primitive, shader_wrapper.vert_indices) diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index f022ecaa..8b7e2ee3 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -218,12 +218,10 @@ class Surface(Mobject): self.uniforms["clip_plane"][:3] = vect if threshold is not None: self.uniforms["clip_plane"][3] = threshold - self.shader_wrapper.use_clip_plane = True return self def deactivate_clip_plane(self): self.uniforms["clip_plane"][:] = 0 - self.shader_wrapper.use_clip_plane = False return self def get_shader_vert_indices(self) -> np.ndarray: diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 8d867128..da45b4df 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -32,7 +32,6 @@ class ShaderWrapper(object): uniforms: dict[str, float | np.ndarray] | None = None, # A dictionary mapping names of uniform variables texture_paths: dict[str, str] | None = None, # A dictionary mapping names to filepaths for textures. depth_test: bool = False, - use_clip_plane: bool = False, render_primitive: int = moderngl.TRIANGLE_STRIP, is_fill: bool = False, ): @@ -43,7 +42,6 @@ class ShaderWrapper(object): self.uniforms = uniforms or dict() self.texture_paths = texture_paths or dict() self.depth_test = depth_test - self.use_clip_plane = use_clip_plane self.render_primitive = str(render_primitive) self.is_fill = is_fill self.init_program_code() @@ -132,6 +130,11 @@ class ShaderWrapper(object): code_map[name] = re.sub(old, new, code_map[name]) self.refresh_id() + def use_clip_plane(self): + if "clip_plane" not in self.uniforms: + return False + return any(self.uniforms["clip_plane"]) + def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: if len(shader_wrappers) > 0: data_list = [self.vert_data, *(sw.vert_data for sw in shader_wrappers)] From 80729c0cb8e719d3aea242518564c2022d61fb27 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 10:37:12 -0800 Subject: [PATCH 03/37] Only initialize ShaderWrappers as needed --- manimlib/mobject/mobject.py | 10 +++++++--- manimlib/mobject/types/vectorized_mobject.py | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 874c81a1..86397bf1 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -101,6 +101,7 @@ class Mobject(object): self.saved_state = None self.target = None self.bounding_box: Vect3Array = np.zeros((3, 3)) + self._shaders_initialized: bool = False self.init_data() self._data_defaults = np.ones(1, dtype=self.data.dtype) @@ -109,7 +110,6 @@ class Mobject(object): self.init_event_listners() self.init_points() self.init_colors() - self.init_shader_data() if self.depth_test: self.apply_depth_test() @@ -1843,7 +1843,6 @@ class Mobject(object): # For shader data def init_shader_data(self): - # TODO, only call this when needed? self.shader_indices = np.zeros(0) self.shader_wrapper = ShaderWrapper( vert_data=self.data, @@ -1854,10 +1853,15 @@ class Mobject(object): ) def refresh_shader_wrapper_id(self): - self.shader_wrapper.refresh_id() + if self._shaders_initialized: + self.shader_wrapper.refresh_id() return self def get_shader_wrapper(self) -> ShaderWrapper: + if not self._shaders_initialized: + self.init_shader_data() + self._shaders_initialized = True + self.shader_wrapper.vert_data = self.get_shader_data() self.shader_wrapper.vert_indices = self.get_shader_vert_indices() self.shader_wrapper.uniforms = self.get_uniforms() diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index aba18c60..8b1f5a58 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1196,6 +1196,10 @@ class VMobject(Mobject): return self def get_shader_wrapper_list(self) -> list[ShaderWrapper]: + if not self._shaders_initialized: + self.init_shader_data() + self._shaders_initialized = True + family = self.family_members_with_points() if not family: return [] From 16d773f1b359ad312b0f88780bcab63968a4178b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 10:48:59 -0800 Subject: [PATCH 04/37] Remove refresh_shader_data --- manimlib/animation/animation.py | 1 - manimlib/mobject/mobject.py | 8 -------- manimlib/mobject/types/vectorized_mobject.py | 3 --- 3 files changed, 12 deletions(-) diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index df05ffea..a8ece9ca 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -65,7 +65,6 @@ class Animation(object): self.rate_func = squish_rate_func( self.rate_func, start / self.run_time, end / self.run_time, ) - self.mobject.refresh_shader_data() self.mobject.set_animating_status(True) self.starting_mobject = self.create_starting_mobject() if self.suspend_mobject_updating: diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 86397bf1..25e6b9f4 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1618,9 +1618,6 @@ class Mobject(object): def align_data(self, mobject: Mobject) -> None: for mob1, mob2 in zip(self.get_family(), mobject.get_family()): - # In case any data arrays get resized when aligned to shader data - mob1.refresh_shader_data() - mob2.refresh_shader_data() mob1.align_points(mob2) def align_points(self, mobject: Mobject): @@ -1731,8 +1728,6 @@ class Mobject(object): """ if self.has_updaters: return - # Be sure shader data has most up to date information - self.refresh_shader_data() self.locked_data_keys = set(keys) def lock_matching_data(self, mobject1: Mobject, mobject2: Mobject): @@ -1888,9 +1883,6 @@ class Mobject(object): def get_shader_data(self): return self.data - def refresh_shader_data(self): - pass - def get_uniforms(self): return self.uniforms diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 8b1f5a58..ed7ac055 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1244,9 +1244,6 @@ class VMobject(Mobject): sw.depth_test = family[0].depth_test return [sw for sw in shader_wrappers if len(sw.vert_data) > 0] - def refresh_shader_data(self): - self.get_shader_wrapper_list() - class VGroup(VMobject): def __init__(self, *vmobjects: VMobject, **kwargs): From 10047773f787bba010493e560cf025660771b736 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 10:49:30 -0800 Subject: [PATCH 05/37] Have ShaderWrapper track OpenGL context --- manimlib/camera/camera.py | 2 +- manimlib/mobject/mobject.py | 22 ++++++++++++++------ manimlib/mobject/types/vectorized_mobject.py | 18 ++++++++++++---- manimlib/shader_wrapper.py | 2 ++ 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index e699acc0..741e80d6 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -337,7 +337,7 @@ class Camera(object): def generate_render_group_list(self, mobject: Mobject) -> Iterable[dict[str, Any]]: return ( self.get_render_group(sw, single_use=mobject.is_changing()) - for sw in mobject.get_shader_wrapper_list() + for sw in mobject.get_shader_wrapper_list(self.ctx) ) def get_render_group( diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 25e6b9f4..14b23d3b 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -51,6 +51,7 @@ if TYPE_CHECKING: from typing import Callable, Iterable, Union, Tuple import numpy.typing as npt from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array + from moderngl.context import Context TimeBasedUpdater = Callable[["Mobject", float], "Mobject" | None] NonTimeUpdater = Callable[["Mobject"], "Mobject" | None] @@ -1837,9 +1838,10 @@ class Mobject(object): # For shader data - def init_shader_data(self): + def init_shader_data(self, ctx: Context): self.shader_indices = np.zeros(0) self.shader_wrapper = ShaderWrapper( + context=ctx, vert_data=self.data, shader_folder=self.shader_folder, texture_paths=self.texture_paths, @@ -1852,9 +1854,9 @@ class Mobject(object): self.shader_wrapper.refresh_id() return self - def get_shader_wrapper(self) -> ShaderWrapper: + def get_shader_wrapper(self, ctx: Context) -> ShaderWrapper: if not self._shaders_initialized: - self.init_shader_data() + self.init_shader_data(ctx) self._shaders_initialized = True self.shader_wrapper.vert_data = self.get_shader_data() @@ -1863,10 +1865,10 @@ class Mobject(object): self.shader_wrapper.depth_test = self.depth_test return self.shader_wrapper - def get_shader_wrapper_list(self) -> list[ShaderWrapper]: + def get_shader_wrapper_list(self, ctx: Context) -> list[ShaderWrapper]: shader_wrappers = it.chain( - [self.get_shader_wrapper()], - *[sm.get_shader_wrapper_list() for sm in self.submobjects] + [self.get_shader_wrapper(ctx)], + *[sm.get_shader_wrapper_list(ctx) for sm in self.submobjects] ) batches = batch_by_property(shader_wrappers, lambda sw: sw.get_id()) @@ -1889,6 +1891,14 @@ class Mobject(object): def get_shader_vert_indices(self): return self.shader_indices + def render(self, ctx: Context, camera_uniforms: dict): + if self.data_has_changed: + self.shader_wrappers = self.get_shader_wrapper_list(ctx) + for shader_wrapper in self.shader_wrappers: + shader_wrapper.update_uniforms(camera_uniforms) + shader_wrapper.update_uniforms(self.get_uniforms) + shader_wrapper.render() + # Event Handlers """ Event handling follows the Event Bubbling model of DOM in javascript. diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index ed7ac055..4fa3df1b 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -46,6 +46,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, Iterable, Tuple from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array + from moderngl.context import Context DEFAULT_STROKE_COLOR = GREY_A DEFAULT_FILL_COLOR = GREY_C @@ -1164,7 +1165,7 @@ class VMobject(Mobject): self.refresh_joint_products() # For shaders - def init_shader_data(self): + def init_shader_data(self, ctx: Context): dtype = self.shader_dtype fill_dtype, stroke_dtype = ( np.dtype([ @@ -1176,6 +1177,7 @@ class VMobject(Mobject): fill_data = np.zeros(0, dtype=fill_dtype) stroke_data = np.zeros(0, dtype=stroke_dtype) self.fill_shader_wrapper = ShaderWrapper( + context=ctx, vert_data=fill_data, uniforms=self.uniforms, shader_folder=self.fill_shader_folder, @@ -1183,21 +1185,29 @@ class VMobject(Mobject): is_fill=True, ) self.stroke_shader_wrapper = ShaderWrapper( + context=ctx, vert_data=stroke_data, uniforms=self.uniforms, shader_folder=self.stroke_shader_folder, render_primitive=self.stroke_render_primitive, ) self.back_stroke_shader_wrapper = self.stroke_shader_wrapper.copy() + self.shader_wrappers = [ + self.back_stroke_shader_wrapper, + self.fill_shader_wrapper, + self.stroke_shader_wrapper, + ] def refresh_shader_wrapper_id(self): - for wrapper in self.get_shader_wrapper_list(): + if not self._shaders_initialized: + return self + for wrapper in self.shader_wrappers: wrapper.refresh_id() return self - def get_shader_wrapper_list(self) -> list[ShaderWrapper]: + def get_shader_wrapper_list(self, ctx: Context) -> list[ShaderWrapper]: if not self._shaders_initialized: - self.init_shader_data() + self.init_shader_data(ctx) self._shaders_initialized = True family = self.family_members_with_points() diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index da45b4df..6239968b 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -26,6 +26,7 @@ if TYPE_CHECKING: class ShaderWrapper(object): def __init__( self, + context: moderngl.context.Context, vert_data: np.ndarray, vert_indices: np.ndarray | None = None, shader_folder: str | None = None, @@ -35,6 +36,7 @@ class ShaderWrapper(object): render_primitive: int = moderngl.TRIANGLE_STRIP, is_fill: bool = False, ): + self.ctx = context self.vert_data = vert_data self.vert_indices = (vert_indices or np.zeros(0)).astype(int) self.vert_attributes = vert_data.dtype.names From 329974135993740805d0edb4fcbea402d2367fcd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 11:23:31 -0800 Subject: [PATCH 06/37] Move program code to ShaderWrapper, away from Camera --- manimlib/camera/camera.py | 20 ++------------------ manimlib/shader_wrapper.py | 38 +++++++++++++++++++++++--------------- manimlib/utils/shaders.py | 19 +++++++++++++++++-- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 741e80d6..abd0bf9a 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -67,7 +67,6 @@ class Camera(object): self.perspective_uniforms = dict() self.init_frame(**frame_config) self.init_context(window) - self.init_shaders() self.init_textures() self.init_light_source() self.refresh_perspective_uniforms() @@ -365,7 +364,8 @@ class Camera(object): vbo = self.ctx.buffer(vert_data) # Program and vertex array - shader_program, vert_format = self.get_shader_program(shader_wrapper) + shader_program = shader_wrapper.program + vert_format = shader_wrapper.vert_format attributes = shader_wrapper.vert_attributes vao = self.ctx.vertex_array( program=shader_program, @@ -392,22 +392,6 @@ class Camera(object): self.mob_to_render_groups = {} # Shaders - def init_shaders(self) -> None: - # Initialize with the null id going to None - self.id_to_shader_program: dict[int, tuple[moderngl.Program, str] | None] = {hash(""): None} - - def get_shader_program( - self, - shader_wrapper: ShaderWrapper - ) -> tuple[moderngl.Program, str] | None: - sid = shader_wrapper.get_program_id() - if sid not in self.id_to_shader_program: - # Create shader program for the first time, then cache - # in the id_to_shader_program dictionary - program = self.ctx.program(**shader_wrapper.get_program_code()) - vert_format = moderngl.detect_format(program, shader_wrapper.vert_attributes) - self.id_to_shader_program[sid] = (program, vert_format) - return self.id_to_shader_program[sid] def set_shader_uniforms( self, diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 6239968b..16ca214a 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -9,6 +9,7 @@ import numpy as np from manimlib.utils.iterables import resize_array from manimlib.utils.shaders import get_shader_code_from_file +from manimlib.utils.shaders import get_shader_program from typing import TYPE_CHECKING @@ -47,8 +48,29 @@ class ShaderWrapper(object): self.render_primitive = str(render_primitive) self.is_fill = is_fill self.init_program_code() + self.init_program() self.refresh_id() + def init_program_code(self) -> None: + def get_code(name: str) -> str | None: + return get_shader_code_from_file( + os.path.join(self.shader_folder, f"{name}.glsl") + ) + + self.program_code: dict[str, str | None] = { + "vertex_shader": get_code("vert"), + "geometry_shader": get_code("geom"), + "fragment_shader": get_code("frag"), + } + + def init_program(self): + if not self.shader_folder: + self.program = None + self.vert_format = None + return + self.program = get_shader_program(self.ctx, **self.program_code) + self.vert_format = moderngl.detect_format(self.program, self.vert_attributes) + def __eq__(self, shader_wrapper: ShaderWrapper): return all(( np.all(self.vert_data == shader_wrapper.vert_data), @@ -109,27 +131,13 @@ class ShaderWrapper(object): for name in ("vertex", "geometry", "fragment") ))) - def init_program_code(self) -> None: - def get_code(name: str) -> str | None: - return get_shader_code_from_file( - os.path.join(self.shader_folder, f"{name}.glsl") - ) - - self.program_code: dict[str, str | None] = { - "vertex_shader": get_code("vert"), - "geometry_shader": get_code("geom"), - "fragment_shader": get_code("frag"), - } - - def get_program_code(self) -> dict[str, str | None]: - return self.program_code - def replace_code(self, old: str, new: str) -> None: code_map = self.program_code for (name, code) in code_map.items(): if code_map[name] is None: continue code_map[name] = re.sub(old, new, code_map[name]) + self.init_program() self.refresh_id() def use_clip_plane(self): diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 2f71aefa..2b4647f0 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -3,6 +3,7 @@ from __future__ import annotations import os import re from functools import lru_cache +import moderngl from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file @@ -10,10 +11,24 @@ from manimlib.utils.file_ops import find_file from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Sequence + from typing import Sequence, Optional -@lru_cache(maxsize=12) +@lru_cache() +def get_shader_program( + ctx: moderngl.context.Context, + vertex_shader: str, + fragment_shader: Optional[str] = None, + geometry_shader: Optional[str] = None, + ) -> moderngl.Program: + return ctx.program( + vertex_shader=vertex_shader, + fragment_shader=fragment_shader, + geometry_shader=geometry_shader, + ) + + +@lru_cache() def get_shader_code_from_file(filename: str) -> str | None: if not filename: return None From c94d8fd3b014b16f544f08045f7b07211e6d111a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 12:10:39 -0800 Subject: [PATCH 07/37] Move Texture handling and vao creation outside of Camera --- manimlib/camera/camera.py | 90 ++------------------ manimlib/mobject/mobject.py | 2 +- manimlib/mobject/types/vectorized_mobject.py | 2 +- manimlib/shader_wrapper.py | 78 ++++++++++++++--- manimlib/utils/shaders.py | 29 +++++++ 5 files changed, 102 insertions(+), 99 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index abd0bf9a..1bd3c4be 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -15,6 +15,7 @@ from manimlib.constants import FRAME_WIDTH from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point from manimlib.utils.color import color_to_rgba +from manimlib.utils.shaders import get_texture_id from typing import TYPE_CHECKING @@ -67,7 +68,6 @@ class Camera(object): self.perspective_uniforms = dict() self.init_frame(**frame_config) self.init_context(window) - self.init_textures() self.init_light_source() self.refresh_perspective_uniforms() self.init_fill_fbo(self.ctx) # Experimental @@ -136,10 +136,8 @@ class Camera(object): ''', ) - tid = self.n_textures - self.fill_texture.use(tid) - self.fill_prog['Texture'].value = tid - self.n_textures += 1 + self.fill_prog['Texture'].value = get_texture_id(self.fill_texture) + verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) self.fill_texture_vao = ctx.simple_vertex_array( self.fill_prog, @@ -289,9 +287,8 @@ class Camera(object): def render(self, render_group: dict[str, Any]) -> None: shader_wrapper = render_group["shader_wrapper"] - shader_program = render_group["prog"] primitive = int(shader_wrapper.render_primitive) - self.set_shader_uniforms(shader_program, shader_wrapper) + shader_wrapper.update_program_uniforms(self.perspective_uniforms) self.set_ctx_depth_test(shader_wrapper.depth_test) self.set_ctx_clip_plane(shader_wrapper.use_clip_plane()) @@ -344,69 +341,20 @@ class Camera(object): shader_wrapper: ShaderWrapper, single_use: bool = True ) -> dict[str, Any]: - # Data buffer - vert_data = shader_wrapper.vert_data - indices = shader_wrapper.vert_indices - if len(indices) == 0: - ibo = None - elif single_use: - ibo = self.ctx.buffer(indices.astype(np.uint32)) - else: - ibo = self.ctx.buffer(indices.astype(np.uint32)) - # # The vao.render call is strangely longer - # # when an index buffer is used, so if the - # # mobject is not changing, meaning only its - # # uniforms are being updated, just create - # # a larger data array based on the indices - # # and don't bother with the ibo - # vert_data = vert_data[indices] - # ibo = None - vbo = self.ctx.buffer(vert_data) - - # Program and vertex array - shader_program = shader_wrapper.program - vert_format = shader_wrapper.vert_format - attributes = shader_wrapper.vert_attributes - vao = self.ctx.vertex_array( - program=shader_program, - content=[(vbo, vert_format, *attributes)], - index_buffer=ibo, - ) return { - "vbo": vbo, - "ibo": ibo, - "vao": vao, - "prog": shader_program, "shader_wrapper": shader_wrapper, + "vao": shader_wrapper.get_vao(single_use), "single_use": single_use, } def release_render_group(self, render_group: dict[str, Any]) -> None: - for key in ["vbo", "ibo", "vao"]: - if render_group[key] is not None: - render_group[key].release() + render_group["shader_wrapper"].release() def refresh_static_mobjects(self) -> None: for render_group in it.chain(*self.mob_to_render_groups.values()): self.release_render_group(render_group) self.mob_to_render_groups = {} - # Shaders - - def set_shader_uniforms( - self, - shader: moderngl.Program, - shader_wrapper: ShaderWrapper - ) -> None: - for name, path in shader_wrapper.texture_paths.items(): - tid = self.get_texture_id(path) - shader[name].value = tid - for name, value in it.chain(self.perspective_uniforms.items(), shader_wrapper.uniforms.items()): - if name in shader: - if isinstance(value, np.ndarray) and value.ndim > 0: - value = tuple(value) - shader[name].value = value - def refresh_perspective_uniforms(self) -> None: frame = self.frame view_matrix = frame.get_view_matrix() @@ -422,32 +370,6 @@ class Camera(object): focal_distance=frame.get_focal_distance(), ) - def init_textures(self) -> None: - self.n_textures: int = 0 - self.path_to_texture: dict[ - str, tuple[int, moderngl.Texture] - ] = {} - - def get_texture_id(self, path: str) -> int: - if path not in self.path_to_texture: - tid = self.n_textures - self.n_textures += 1 - im = Image.open(path).convert("RGBA") - texture = self.ctx.texture( - size=im.size, - components=len(im.getbands()), - data=im.tobytes(), - ) - texture.use(location=tid) - self.path_to_texture[path] = (tid, texture) - return self.path_to_texture[path][0] - - def release_texture(self, path: str): - tid_and_texture = self.path_to_texture.pop(path, None) - if tid_and_texture: - tid_and_texture[1].release() - return self - # Mostly just defined so old scenes don't break class ThreeDCamera(Camera): diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 14b23d3b..f5de726d 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1861,7 +1861,7 @@ class Mobject(object): self.shader_wrapper.vert_data = self.get_shader_data() self.shader_wrapper.vert_indices = self.get_shader_vert_indices() - self.shader_wrapper.uniforms = self.get_uniforms() + self.shader_wrapper.uniforms.update(self.get_uniforms()) self.shader_wrapper.depth_test = self.depth_test return self.shader_wrapper diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 4fa3df1b..d9cb4d2c 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1250,7 +1250,7 @@ class VMobject(Mobject): for sw in shader_wrappers: # Assume uniforms of the first family member - sw.uniforms = family[0].get_uniforms() + sw.uniforms.update(family[0].get_uniforms()) sw.depth_test = family[0].depth_test return [sw for sw in shader_wrappers if len(sw.vert_data) > 0] diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 16ca214a..8f02a977 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -10,11 +10,14 @@ import numpy as np from manimlib.utils.iterables import resize_array from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_program +from manimlib.utils.shaders import image_path_to_texture +from manimlib.utils.shaders import get_texture_id +from manimlib.utils.shaders import release_texture from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import List + from typing import List, Optional # Mobjects that should be rendered with @@ -29,10 +32,10 @@ class ShaderWrapper(object): self, context: moderngl.context.Context, vert_data: np.ndarray, - vert_indices: np.ndarray | None = None, - shader_folder: str | None = None, - uniforms: dict[str, float | np.ndarray] | None = None, # A dictionary mapping names of uniform variables - texture_paths: dict[str, str] | None = None, # A dictionary mapping names to filepaths for textures. + vert_indices: Optional[np.ndarray] = None, + shader_folder: Optional[str] = None, + uniforms: Optional[dict[str, float | np.ndarray]] = None, # A dictionary mapping names of uniform variables + texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures. depth_test: bool = False, render_primitive: int = moderngl.TRIANGLE_STRIP, is_fill: bool = False, @@ -43,12 +46,18 @@ class ShaderWrapper(object): self.vert_attributes = vert_data.dtype.names self.shader_folder = shader_folder self.uniforms = uniforms or dict() - self.texture_paths = texture_paths or dict() self.depth_test = depth_test self.render_primitive = str(render_primitive) self.is_fill = is_fill + + self.vbo = None + self.ibo = None + self.vao = None + self.init_program_code() self.init_program() + if texture_paths is not None: + self.init_textures(texture_paths) self.refresh_id() def init_program_code(self) -> None: @@ -71,6 +80,12 @@ class ShaderWrapper(object): self.program = get_shader_program(self.ctx, **self.program_code) self.vert_format = moderngl.detect_format(self.program, self.vert_attributes) + def init_textures(self, texture_paths: dict[str, str]): + for name, path in texture_paths.items(): + texture = image_path_to_texture(path, self.ctx) + tid = get_texture_id(texture) + self.uniforms[name] = tid + def __eq__(self, shader_wrapper: ShaderWrapper): return all(( np.all(self.vert_data == shader_wrapper.vert_data), @@ -80,10 +95,6 @@ class ShaderWrapper(object): np.all(self.uniforms[key] == shader_wrapper.uniforms[key]) for key in self.uniforms ), - all( - self.texture_paths[key] == shader_wrapper.texture_paths[key] - for key in self.texture_paths - ), self.depth_test == shader_wrapper.depth_test, self.render_primitive == shader_wrapper.render_primitive, )) @@ -94,8 +105,6 @@ class ShaderWrapper(object): result.vert_indices = self.vert_indices.copy() if self.uniforms: result.uniforms = {key: np.array(value) for key, value in self.uniforms.items()} - if self.texture_paths: - result.texture_paths = dict(self.texture_paths) return result def is_valid(self) -> bool: @@ -116,7 +125,6 @@ class ShaderWrapper(object): return "|".join(map(str, [ self.program_id, self.uniforms, - self.texture_paths, self.depth_test, self.render_primitive, ])) @@ -186,3 +194,47 @@ class ShaderWrapper(object): n_verts = new_n_verts n_points += len(data) return self + + def update_program_uniforms(self, camera_uniforms: dict): + if self.program is None: + return + for name, value in (*camera_uniforms.items(), *self.uniforms.items()): + if name in self.program: + if isinstance(value, np.ndarray) and value.ndim > 0: + value = tuple(value) + self.program[name].value = value + + def get_vao(self, single_use: bool = False): + # Data buffer + vert_data = self.vert_data + indices = self.vert_indices + if len(indices) == 0: + self.ibo = None + elif single_use or self.is_fill: + self.ibo = self.ctx.buffer(indices.astype(np.uint32)) + else: + # The vao.render call is strangely longer + # when an index buffer is used, so if the + # mobject is not changing, meaning only its + # uniforms are being updated, just create + # a larger data array based on the indices + # and don't bother with the ibo + vert_data = vert_data[indices] + self.ibo = None + self.vbo = self.ctx.buffer(vert_data) + + # Vertex array object + self.vao = self.ctx.vertex_array( + program=self.program, + content=[(self.vbo, self.vert_format, *self.vert_attributes)], + index_buffer=self.ibo, + ) + return self.vao + + def release(self): + for obj in (self.vbo, self.ibo, self.vao): + if obj is not None: + obj.release() + self.vbo = None + self.ibo = None + self.vao = None diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 2b4647f0..eaddcaea 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -4,6 +4,7 @@ import os import re from functools import lru_cache import moderngl +from PIL import Image from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file @@ -14,6 +15,34 @@ if TYPE_CHECKING: from typing import Sequence, Optional +ID_TO_TEXTURE: dict[int, moderngl.Texture] = dict() + + +@lru_cache() +def image_path_to_texture(path: str, ctx: moderngl.Context) -> moderngl.Texture: + im = Image.open(path).convert("RGBA") + return ctx.texture( + size=im.size, + components=len(im.getbands()), + data=im.tobytes(), + ) + + +def get_texture_id(texture: moderngl.Texture) -> int: + tid = 0 + while tid in ID_TO_TEXTURE: + tid += 1 + ID_TO_TEXTURE[tid] = texture + texture.use(location=tid) + return tid + + +def release_texture(texture_id: int): + texture = ID_TO_TEXTURE.pop(texture_id, None) + if texture is not None: + texture.release() + + @lru_cache() def get_shader_program( ctx: moderngl.context.Context, From 2c737ed5406bb77d51e04e9cc7eb09f96faf1488 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 13:45:18 -0800 Subject: [PATCH 08/37] Move most of rendering logic to ShaderWrapper --- manimlib/camera/camera.py | 102 +------------ manimlib/mobject/mobject.py | 2 +- manimlib/mobject/types/vectorized_mobject.py | 8 +- manimlib/shader_wrapper.py | 151 ++++++++++++++++--- 4 files changed, 134 insertions(+), 129 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 1bd3c4be..6aac7a81 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -70,7 +70,6 @@ class Camera(object): self.init_context(window) self.init_light_source() self.refresh_perspective_uniforms() - self.init_fill_fbo(self.ctx) # Experimental # A cached map from mobjects to their associated list of render groups # so that these render groups are not regenerated unnecessarily for static # mobjects @@ -87,80 +86,13 @@ class Camera(object): self.ctx = window.ctx self.fbo = self.ctx.detect_framebuffer() self.fbo.use() - self.set_ctx_blending() self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) + self.ctx.enable(moderngl.BLEND) # This is the frame buffer we'll draw into when emitting frames self.draw_fbo = self.get_fbo(samples=0) - def init_fill_fbo(self, ctx: moderngl.context.Context): - # Experimental - size = self.get_pixel_shape() - self.fill_texture = ctx.texture( - size=size, - components=4, - # Important to make sure floating point (not fixed point) is - # used so that alpha values are not clipped - dtype='f2', - ) - # TODO, depth buffer is not really used yet - fill_depth = ctx.depth_renderbuffer(size) - self.fill_fbo = ctx.framebuffer(self.fill_texture, fill_depth) - self.fill_prog = ctx.program( - vertex_shader=''' - #version 330 - - in vec2 texcoord; - out vec2 v_textcoord; - - void main() { - gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0); - v_textcoord = texcoord; - } - ''', - fragment_shader=''' - #version 330 - - uniform sampler2D Texture; - - in vec2 v_textcoord; - out vec4 frag_color; - - void main() { - frag_color = texture(Texture, v_textcoord); - frag_color = abs(frag_color); - if(frag_color.a == 0) discard; - //TODO, set gl_FragDepth; - } - ''', - ) - - self.fill_prog['Texture'].value = get_texture_id(self.fill_texture) - - verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) - self.fill_texture_vao = ctx.simple_vertex_array( - self.fill_prog, - ctx.buffer(verts.astype('f4').tobytes()), - 'texcoord', - ) - - def set_ctx_blending(self, enable: bool = True) -> None: - if enable: - self.ctx.enable(moderngl.BLEND) - else: - self.ctx.disable(moderngl.BLEND) - - def set_ctx_depth_test(self, enable: bool = True) -> None: - if enable: - self.ctx.enable(moderngl.DEPTH_TEST) - else: - self.ctx.disable(moderngl.DEPTH_TEST) - - def set_ctx_clip_plane(self, enable: bool = True) -> None: - if enable: - gl.glEnable(gl.GL_CLIP_DISTANCE0) - def init_light_source(self) -> None: self.light_source = Point(self.light_source_position) @@ -287,39 +219,11 @@ class Camera(object): def render(self, render_group: dict[str, Any]) -> None: shader_wrapper = render_group["shader_wrapper"] - primitive = int(shader_wrapper.render_primitive) - shader_wrapper.update_program_uniforms(self.perspective_uniforms) - self.set_ctx_depth_test(shader_wrapper.depth_test) - self.set_ctx_clip_plane(shader_wrapper.use_clip_plane()) - - if shader_wrapper.is_fill: - self.render_fill(render_group["vao"], primitive, shader_wrapper.vert_indices) - else: - render_group["vao"].render(primitive) + shader_wrapper.render(self.perspective_uniforms) if render_group["single_use"]: self.release_render_group(render_group) - def render_fill(self, vao, render_primitive: int, indices: np.ndarray): - """ - VMobject fill is handled in a special way, where emited triangles - must be blended with moderngl.FUNC_SUBTRACT so as to effectively compute - a winding number around each pixel. This is rendered to a separate texture, - then that texture is overlayed onto the current fbo - """ - winding = (len(indices) == 0) - vao.program['winding'].value = winding - if not winding: - vao.render(moderngl.TRIANGLES) - return - self.fill_fbo.clear() - self.fill_fbo.use() - self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) - vao.render(render_primitive) - self.ctx.blend_func = moderngl.DEFAULT_BLENDING - self.fbo.use() - self.fill_texture_vao.render(moderngl.TRIANGLE_STRIP) - def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str, Any]]: if mobject.is_changing(): return self.generate_render_group_list(mobject) @@ -341,9 +245,9 @@ class Camera(object): shader_wrapper: ShaderWrapper, single_use: bool = True ) -> dict[str, Any]: + shader_wrapper.get_vao() return { "shader_wrapper": shader_wrapper, - "vao": shader_wrapper.get_vao(single_use), "single_use": single_use, } diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index f5de726d..69bdd88c 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1841,7 +1841,7 @@ class Mobject(object): def init_shader_data(self, ctx: Context): self.shader_indices = np.zeros(0) self.shader_wrapper = ShaderWrapper( - context=ctx, + ctx=ctx, vert_data=self.data, shader_folder=self.shader_folder, texture_paths=self.texture_paths, diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index d9cb4d2c..c04577ed 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -40,6 +40,7 @@ from manimlib.utils.space_ops import midpoint from manimlib.utils.space_ops import normalize_along_axis from manimlib.utils.space_ops import z_to_vector from manimlib.shader_wrapper import ShaderWrapper +from manimlib.shader_wrapper import FillShaderWrapper from typing import TYPE_CHECKING @@ -1176,16 +1177,15 @@ class VMobject(Mobject): ) fill_data = np.zeros(0, dtype=fill_dtype) stroke_data = np.zeros(0, dtype=stroke_dtype) - self.fill_shader_wrapper = ShaderWrapper( - context=ctx, + self.fill_shader_wrapper = FillShaderWrapper( + ctx=ctx, vert_data=fill_data, uniforms=self.uniforms, shader_folder=self.fill_shader_folder, render_primitive=self.fill_render_primitive, - is_fill=True, ) self.stroke_shader_wrapper = ShaderWrapper( - context=ctx, + ctx=ctx, vert_data=stroke_data, uniforms=self.uniforms, shader_folder=self.stroke_shader_folder, diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 8f02a977..ce95c090 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -4,9 +4,12 @@ import copy import os import re +import OpenGL.GL as gl import moderngl import numpy as np +from manimlib.constants import DEFAULT_PIXEL_HEIGHT +from manimlib.constants import DEFAULT_PIXEL_WIDTH from manimlib.utils.iterables import resize_array from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_program @@ -30,7 +33,7 @@ if TYPE_CHECKING: class ShaderWrapper(object): def __init__( self, - context: moderngl.context.Context, + ctx: moderngl.context.Context, vert_data: np.ndarray, vert_indices: Optional[np.ndarray] = None, shader_folder: Optional[str] = None, @@ -38,17 +41,15 @@ class ShaderWrapper(object): texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures. depth_test: bool = False, render_primitive: int = moderngl.TRIANGLE_STRIP, - is_fill: bool = False, ): - self.ctx = context + self.ctx = ctx self.vert_data = vert_data self.vert_indices = (vert_indices or np.zeros(0)).astype(int) self.vert_attributes = vert_data.dtype.names self.shader_folder = shader_folder self.uniforms = uniforms or dict() self.depth_test = depth_test - self.render_primitive = str(render_primitive) - self.is_fill = is_fill + self.render_primitive = render_primitive self.vbo = None self.ibo = None @@ -99,6 +100,9 @@ class ShaderWrapper(object): self.render_primitive == shader_wrapper.render_primitive, )) + def __del__(self): + self.release() + def copy(self): result = copy.copy(self) result.vert_data = self.vert_data.copy() @@ -148,11 +152,33 @@ class ShaderWrapper(object): self.init_program() self.refresh_id() + # Changing context def use_clip_plane(self): if "clip_plane" not in self.uniforms: return False return any(self.uniforms["clip_plane"]) + def set_ctx_depth_test(self, enable: bool = True) -> None: + if enable: + self.ctx.enable(moderngl.DEPTH_TEST) + else: + self.ctx.disable(moderngl.DEPTH_TEST) + + def set_ctx_clip_plane(self, enable: bool = True) -> None: + if enable: + gl.glEnable(gl.GL_CLIP_DISTANCE0) + + + # Related to data and rendering + def render(self, camera_uniforms: dict): + self.update_program_uniforms(camera_uniforms) + self.set_ctx_depth_test(self.depth_test) + self.set_ctx_clip_plane(self.use_clip_plane()) + + # TODO, generate on the fly? + assert(self.vao is not None) + self.vao.render(self.render_primitive) + def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: if len(shader_wrappers) > 0: data_list = [self.vert_data, *(sw.vert_data for sw in shader_wrappers)] @@ -204,30 +230,25 @@ class ShaderWrapper(object): value = tuple(value) self.program[name].value = value - def get_vao(self, single_use: bool = False): - # Data buffer - vert_data = self.vert_data - indices = self.vert_indices - if len(indices) == 0: - self.ibo = None - elif single_use or self.is_fill: - self.ibo = self.ctx.buffer(indices.astype(np.uint32)) - else: - # The vao.render call is strangely longer - # when an index buffer is used, so if the - # mobject is not changing, meaning only its - # uniforms are being updated, just create - # a larger data array based on the indices - # and don't bother with the ibo - vert_data = vert_data[indices] - self.ibo = None - self.vbo = self.ctx.buffer(vert_data) + def get_vertex_buffer_object(self, refresh: bool = True): + if refresh: + self.vbo = self.ctx.buffer(self.vert_data) + return self.vbo + def get_index_buffer_object(self, refresh: bool = True): + if refresh and len(self.vert_indices) > 0: + self.ibo = self.ctx.buffer(self.vert_indices.astype(np.uint32)) + return self.ibo + + def get_vao(self, refresh: bool = True): + # Data buffer + vbo = self.get_vertex_buffer_object(refresh) + ibo = self.get_index_buffer_object(refresh) # Vertex array object self.vao = self.ctx.vertex_array( program=self.program, - content=[(self.vbo, self.vert_format, *self.vert_attributes)], - index_buffer=self.ibo, + content=[(vbo, self.vert_format, *self.vert_attributes)], + index_buffer=ibo, ) return self.vao @@ -238,3 +259,83 @@ class ShaderWrapper(object): self.vbo = None self.ibo = None self.vao = None + + +class FillShaderWrapper(ShaderWrapper): + def __init__( + self, + ctx: moderngl.context.Context, + *args, + **kwargs + ): + super().__init__(ctx, *args, **kwargs) + + size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) + self.fill_texture = ctx.texture( + size=size, + components=4, + # Important to make sure floating point (not fixed point) is + # used so that alpha values are not clipped + dtype='f2', + ) + # TODO, depth buffer is not really used yet + fill_depth = ctx.depth_renderbuffer(size) + self.fill_fbo = ctx.framebuffer(self.fill_texture, fill_depth) + self.fill_prog = ctx.program( + vertex_shader=''' + #version 330 + + in vec2 texcoord; + out vec2 v_textcoord; + + void main() { + gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0); + v_textcoord = texcoord; + } + ''', + fragment_shader=''' + #version 330 + + uniform sampler2D Texture; + + in vec2 v_textcoord; + out vec4 frag_color; + + void main() { + frag_color = texture(Texture, v_textcoord); + frag_color = abs(frag_color); + if(frag_color.a == 0) discard; + //TODO, set gl_FragDepth; + } + ''', + ) + + self.fill_prog['Texture'].value = get_texture_id(self.fill_texture) + + verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) + self.fill_texture_vao = ctx.simple_vertex_array( + self.fill_prog, + ctx.buffer(verts.astype('f4').tobytes()), + 'texcoord', + ) + + def render(self, camera_uniforms: dict): + # TODO, these are copied... + self.update_program_uniforms(camera_uniforms) + self.set_ctx_depth_test(self.depth_test) + self.set_ctx_clip_plane(self.use_clip_plane()) + # + vao = self.vao + assert(vao is not None) + winding = (len(self.vert_indices) == 0) + vao.program['winding'].value = winding + if not winding: + vao.render(moderngl.TRIANGLES) + return + self.fill_fbo.clear() + self.fill_fbo.use() + self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) + vao.render(self.render_primitive) + self.ctx.blend_func = moderngl.DEFAULT_BLENDING + self.ctx.screen.use() + self.fill_texture_vao.render(moderngl.TRIANGLE_STRIP) From 424707d035f3d5faf050d8de6b41d49e8c4c5e0c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 14:13:56 -0800 Subject: [PATCH 09/37] Move rendering more fully away from Camera to Mobject and ShaderWrapper --- manimlib/camera/camera.py | 58 ++++--------------------------------- manimlib/mobject/mobject.py | 12 ++++++-- manimlib/scene/scene.py | 3 +- manimlib/shader_wrapper.py | 41 +++++++++++++------------- 4 files changed, 37 insertions(+), 77 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 6aac7a81..fa65c6cd 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -65,15 +65,10 @@ class Camera(object): self.background_rgba: list[float] = list(color_to_rgba( background_color, background_opacity )) - self.perspective_uniforms = dict() + self.uniforms = dict() self.init_frame(**frame_config) self.init_context(window) self.init_light_source() - self.refresh_perspective_uniforms() - # A cached map from mobjects to their associated list of render groups - # so that these render groups are not regenerated unnecessarily for static - # mobjects - self.mob_to_render_groups = {} def init_frame(self, **config) -> None: self.frame = CameraFrame(**config) @@ -212,60 +207,17 @@ class Camera(object): # Rendering def capture(self, *mobjects: Mobject) -> None: - self.refresh_perspective_uniforms() + self.refresh_uniforms() for mobject in mobjects: - for render_group in self.get_render_group_list(mobject): - self.render(render_group) + mobject.render(self.ctx, self.uniforms) - def render(self, render_group: dict[str, Any]) -> None: - shader_wrapper = render_group["shader_wrapper"] - shader_wrapper.render(self.perspective_uniforms) - - if render_group["single_use"]: - self.release_render_group(render_group) - - def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str, Any]]: - if mobject.is_changing(): - return self.generate_render_group_list(mobject) - - # Otherwise, cache result for later use - key = id(mobject) - if key not in self.mob_to_render_groups: - self.mob_to_render_groups[key] = list(self.generate_render_group_list(mobject)) - return self.mob_to_render_groups[key] - - def generate_render_group_list(self, mobject: Mobject) -> Iterable[dict[str, Any]]: - return ( - self.get_render_group(sw, single_use=mobject.is_changing()) - for sw in mobject.get_shader_wrapper_list(self.ctx) - ) - - def get_render_group( - self, - shader_wrapper: ShaderWrapper, - single_use: bool = True - ) -> dict[str, Any]: - shader_wrapper.get_vao() - return { - "shader_wrapper": shader_wrapper, - "single_use": single_use, - } - - def release_render_group(self, render_group: dict[str, Any]) -> None: - render_group["shader_wrapper"].release() - - def refresh_static_mobjects(self) -> None: - for render_group in it.chain(*self.mob_to_render_groups.values()): - self.release_render_group(render_group) - self.mob_to_render_groups = {} - - def refresh_perspective_uniforms(self) -> None: + def refresh_uniforms(self) -> None: frame = self.frame view_matrix = frame.get_view_matrix() light_pos = self.light_source.get_location() cam_pos = self.frame.get_implied_camera_location() - self.perspective_uniforms.update( + self.uniforms.update( frame_shape=frame.get_shape(), pixel_size=self.get_pixel_size(), view=tuple(view_matrix.T.flatten()), diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 69bdd88c..93162bec 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -103,6 +103,7 @@ class Mobject(object): self.target = None self.bounding_box: Vect3Array = np.zeros((3, 3)) self._shaders_initialized: bool = False + self._data_has_changed: bool = True self.init_data() self._data_defaults = np.ones(1, dtype=self.data.dtype) @@ -1892,11 +1893,16 @@ class Mobject(object): return self.shader_indices def render(self, ctx: Context, camera_uniforms: dict): - if self.data_has_changed: + if self._data_has_changed or self.is_changing(): self.shader_wrappers = self.get_shader_wrapper_list(ctx) + for shader_wrapper in self.shader_wrappers: + shader_wrapper.release() + shader_wrapper.get_vao() + self._data_has_changed = False for shader_wrapper in self.shader_wrappers: - shader_wrapper.update_uniforms(camera_uniforms) - shader_wrapper.update_uniforms(self.get_uniforms) + shader_wrapper.uniforms.update(self.get_uniforms()) + shader_wrapper.uniforms.update(camera_uniforms) + shader_wrapper.pre_render() shader_wrapper.render() # Event Handlers diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a37b057e..53d33d8d 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -575,7 +575,8 @@ class Scene(object): self.num_plays += 1 def refresh_static_mobjects(self) -> None: - self.camera.refresh_static_mobjects() + for mobject in self.mobjects: + mobject._data_has_changed = True def begin_animations(self, animations: Iterable[Animation]) -> None: for animation in animations: diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index ce95c090..ac02131e 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -47,7 +47,7 @@ class ShaderWrapper(object): self.vert_indices = (vert_indices or np.zeros(0)).astype(int) self.vert_attributes = vert_data.dtype.names self.shader_folder = shader_folder - self.uniforms = uniforms or dict() + self.uniforms = dict(uniforms or {}) self.depth_test = depth_test self.render_primitive = render_primitive @@ -168,16 +168,7 @@ class ShaderWrapper(object): if enable: gl.glEnable(gl.GL_CLIP_DISTANCE0) - - # Related to data and rendering - def render(self, camera_uniforms: dict): - self.update_program_uniforms(camera_uniforms) - self.set_ctx_depth_test(self.depth_test) - self.set_ctx_clip_plane(self.use_clip_plane()) - - # TODO, generate on the fly? - assert(self.vao is not None) - self.vao.render(self.render_primitive) + # Adding data def combine_with(self, *shader_wrappers: ShaderWrapper) -> ShaderWrapper: if len(shader_wrappers) > 0: @@ -221,10 +212,21 @@ class ShaderWrapper(object): n_points += len(data) return self - def update_program_uniforms(self, camera_uniforms: dict): + # Related to data and rendering + def pre_render(self): + self.set_ctx_depth_test(self.depth_test) + self.set_ctx_clip_plane(self.use_clip_plane()) + self.update_program_uniforms() + + def render(self): + # TODO, generate on the fly? + assert(self.vao is not None) + self.vao.render() + + def update_program_uniforms(self): if self.program is None: return - for name, value in (*camera_uniforms.items(), *self.uniforms.items()): + for name, value in self.uniforms.items(): if name in self.program: if isinstance(value, np.ndarray) and value.ndim > 0: value = tuple(value) @@ -249,13 +251,17 @@ class ShaderWrapper(object): program=self.program, content=[(vbo, self.vert_format, *self.vert_attributes)], index_buffer=ibo, + mode=self.render_primitive, ) return self.vao def release(self): for obj in (self.vbo, self.ibo, self.vao): if obj is not None: - obj.release() + try: + obj.release() + except AttributeError: + pass self.vbo = None self.ibo = None self.vao = None @@ -319,12 +325,7 @@ class FillShaderWrapper(ShaderWrapper): 'texcoord', ) - def render(self, camera_uniforms: dict): - # TODO, these are copied... - self.update_program_uniforms(camera_uniforms) - self.set_ctx_depth_test(self.depth_test) - self.set_ctx_clip_plane(self.use_clip_plane()) - # + def render(self): vao = self.vao assert(vao is not None) winding = (len(self.vert_indices) == 0) From 4dfabc1c28d965429a18926028cf8efa63f20711 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 14:20:36 -0800 Subject: [PATCH 10/37] Make sure FillShaderWrapper works without a window --- manimlib/shader_wrapper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index ac02131e..b7e311b1 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -333,10 +333,11 @@ class FillShaderWrapper(ShaderWrapper): if not winding: vao.render(moderngl.TRIANGLES) return + original_fbo = self.ctx.fbo self.fill_fbo.clear() self.fill_fbo.use() self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) vao.render(self.render_primitive) self.ctx.blend_func = moderngl.DEFAULT_BLENDING - self.ctx.screen.use() + original_fbo.use() self.fill_texture_vao.render(moderngl.TRIANGLE_STRIP) From d2af6a5f4bbbe03653b221b491b408138887fb8e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 16:43:47 -0800 Subject: [PATCH 11/37] Keep track of when Mobject data has changed, and used that to determine when ShaderWrapper generates new buffers --- manimlib/animation/creation.py | 14 ------ manimlib/mobject/mobject.py | 49 +++++++++++++++---- manimlib/mobject/types/dot_cloud.py | 3 ++ manimlib/mobject/types/image_mobject.py | 1 + manimlib/mobject/types/point_cloud_mobject.py | 7 ++- manimlib/mobject/types/surface.py | 4 ++ manimlib/mobject/types/vectorized_mobject.py | 10 +++- manimlib/scene/scene.py | 11 ----- manimlib/shader_wrapper.py | 24 +++++---- 9 files changed, 74 insertions(+), 49 deletions(-) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index 8271eb2c..cec23ab0 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -30,15 +30,6 @@ class ShowPartial(Animation, ABC): self.should_match_start = should_match_start super().__init__(mobject, **kwargs) - def begin(self) -> None: - super().begin() - if not self.should_match_start: - self.mobject.lock_matching_data(self.mobject, self.starting_mobject) - - def finish(self) -> None: - super().finish() - self.mobject.unlock_data() - def interpolate_submobject( self, submob: VMobject, @@ -114,11 +105,9 @@ class DrawBorderThenFill(Animation): self.outline = self.get_outline() super().begin() self.mobject.match_style(self.outline) - self.mobject.lock_matching_data(self.mobject, self.outline) def finish(self) -> None: super().finish() - self.mobject.unlock_data() self.mobject.refresh_joint_products() def get_outline(self) -> VMobject: @@ -146,9 +135,6 @@ class DrawBorderThenFill(Animation): if index == 1 and self.sm_to_index[hash(submob)] == 0: # First time crossing over submob.set_data(outline.data) - submob.unlock_data() - if not self.mobject.has_updaters: - submob.lock_matching_data(submob, start) submob.needs_new_triangulation = False self.sm_to_index[hash(submob)] = 1 diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 93162bec..5c1b1576 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -143,11 +143,6 @@ class Mobject(object): # Typically implemented in subclass, unlpess purposefully left blank pass - def set_data(self, data: np.ndarray): - assert(data.dtype == self.data.dtype) - self.data = data - return self - def set_uniforms(self, uniforms: dict): for key, value in uniforms.items(): if isinstance(value, np.ndarray): @@ -160,8 +155,36 @@ class Mobject(object): # Borrowed from https://github.com/ManimCommunity/manim/ return _AnimationBuilder(self) - # Only these methods should directly affect points + def note_changed_data(self, recurse_up: bool = True): + self._data_has_changed = True + if recurse_up: + for mob in self.parents: + mob.note_changed_data() + def affects_data(func: Callable): + @wraps(func) + def wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + self.note_changed_data() + return wrapper + + def affects_family_data(func: Callable): + @wraps(func) + def wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + for mob in self.family_members_with_points(): + mob.note_changed_data() + return self + return wrapper + + # Only these methods should directly affect points + @affects_data + def set_data(self, data: np.ndarray): + assert(data.dtype == self.data.dtype) + self.data = data + return self + + @affects_data def resize_points( self, new_length: int, @@ -177,11 +200,13 @@ class Mobject(object): self.refresh_bounding_box() return self + @affects_data def set_points(self, points: Vect3Array): self.resize_points(len(points), resize_func=resize_preserving_order) self.data["point"][:] = points return self + @affects_data def append_points(self, new_points: Vect3Array): n = self.get_num_points() self.resize_points(n + len(new_points)) @@ -192,11 +217,13 @@ class Mobject(object): self.refresh_bounding_box() return self + @affects_family_data def reverse_points(self): for mob in self.get_family(): mob.data = mob.data[::-1] return self + @affects_family_data def apply_points_function( self, func: Callable[[np.ndarray], np.ndarray], @@ -330,6 +357,7 @@ class Mobject(object): def split(self) -> list[Mobject]: return self.submobjects + @affects_data def assemble_family(self): sub_families = (sm.get_family() for sm in self.submobjects) self.family = [self, *it.chain(*sub_families)] @@ -593,6 +621,7 @@ class Mobject(object): # won't have changed, just directly match. result.non_time_updaters = list(self.non_time_updaters) result.time_based_updaters = list(self.time_based_updaters) + result._data_has_changed = True family = self.get_family() for attr, value in list(self.__dict__.items()): @@ -1216,6 +1245,7 @@ class Mobject(object): # Color functions + @affects_family_data def set_rgba_array( self, rgba_array: npt.ArrayLike, @@ -1254,6 +1284,7 @@ class Mobject(object): mob.set_rgba_array(rgba_array) return self + @affects_family_data def set_rgba_array_by_color( self, color: ManimColor | Iterable[ManimColor] | None = None, @@ -1681,6 +1712,7 @@ class Mobject(object): # Interpolate + @affects_data def interpolate( self, mobject1: Mobject, @@ -1893,11 +1925,10 @@ class Mobject(object): return self.shader_indices def render(self, ctx: Context, camera_uniforms: dict): - if self._data_has_changed or self.is_changing(): + if self._data_has_changed: self.shader_wrappers = self.get_shader_wrapper_list(ctx) for shader_wrapper in self.shader_wrappers: - shader_wrapper.release() - shader_wrapper.get_vao() + shader_wrapper.generate_vao() self._data_has_changed = False for shader_wrapper in self.shader_wrappers: shader_wrapper.uniforms.update(self.get_uniforms()) diff --git a/manimlib/mobject/types/dot_cloud.py b/manimlib/mobject/types/dot_cloud.py index 613c6e0b..83aa9b07 100644 --- a/manimlib/mobject/types/dot_cloud.py +++ b/manimlib/mobject/types/dot_cloud.py @@ -5,6 +5,7 @@ import numpy as np from manimlib.constants import GREY_C, YELLOW from manimlib.constants import ORIGIN, NULL_POINTS +from manimlib.mobject.mobject import Mobject from manimlib.mobject.types.point_cloud_mobject import PMobject from manimlib.utils.iterables import resize_with_interpolation @@ -94,6 +95,7 @@ class DotCloud(PMobject): self.center() return self + @Mobject.affects_data def set_radii(self, radii: npt.ArrayLike): n_points = self.get_num_points() radii = np.array(radii).reshape((len(radii), 1)) @@ -104,6 +106,7 @@ class DotCloud(PMobject): def get_radii(self) -> np.ndarray: return self.data["radius"] + @Mobject.affects_data def set_radius(self, radius: float): data = self.data if self.get_num_points() > 0 else self._data_defaults data["radius"][:] = radius diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index 8de5b10e..80efe5aa 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -47,6 +47,7 @@ class ImageMobject(Mobject): self.set_width(2 * size[0] / size[1], stretch=True) self.set_height(self.height) + @Mobject.affects_data def set_opacity(self, opacity: float, recurse: bool = True): self.data["opacity"][:, 0] = resize_with_interpolation( np.array(listify(opacity)), diff --git a/manimlib/mobject/types/point_cloud_mobject.py b/manimlib/mobject/types/point_cloud_mobject.py index ee1b384b..f6180b3f 100644 --- a/manimlib/mobject/types/point_cloud_mobject.py +++ b/manimlib/mobject/types/point_cloud_mobject.py @@ -5,7 +5,6 @@ import numpy as np from manimlib.mobject.mobject import Mobject from manimlib.utils.color import color_gradient from manimlib.utils.color import color_to_rgba -from manimlib.utils.iterables import resize_array from manimlib.utils.iterables import resize_with_interpolation from typing import TYPE_CHECKING @@ -52,6 +51,7 @@ class PMobject(Mobject): self.add_points([point], rgbas, color, opacity) return self + @Mobject.affects_data def set_color_by_gradient(self, *colors: ManimColor): self.data["rgba"][:] = np.array(list(map( color_to_rgba, @@ -59,17 +59,20 @@ class PMobject(Mobject): ))) return self + @Mobject.affects_data def match_colors(self, pmobject: PMobject): self.data["rgba"][:] = resize_with_interpolation( pmobject.data["rgba"], self.get_num_points() ) return self + @Mobject.affects_data def filter_out(self, condition: Callable[[np.ndarray], bool]): for mob in self.family_members_with_points(): mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())] return self + @Mobject.affects_data def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]): """ function is any map from R^3 to R @@ -81,6 +84,7 @@ class PMobject(Mobject): mob.data[:] = mob.data[indices] return self + @Mobject.affects_data def ingest_submobjects(self): self.data = np.vstack([ sm.data for sm in self.get_family() @@ -91,6 +95,7 @@ class PMobject(Mobject): index = alpha * (self.get_num_points() - 1) return self.get_points()[int(index)] + @Mobject.affects_data def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float): lower_index = int(a * pmobject.get_num_points()) upper_index = int(b * pmobject.get_num_points()) diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index 8b7e2ee3..81855bac 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -72,6 +72,7 @@ class Surface(Mobject): # To be implemented in subclasses return (u, v, 0.0) + @Mobject.affects_data def init_points(self): dim = self.dim nu, nv = self.resolution @@ -130,6 +131,7 @@ class Surface(Mobject): ) return normalize_along_axis(normals, 1) + @Mobject.affects_data def pointwise_become_partial( self, smobject: "Surface", @@ -298,6 +300,7 @@ class TexturedSurface(Surface): **kwargs ) + @Mobject.affects_data def init_points(self): surf = self.uv_surface nu, nv = surf.resolution @@ -315,6 +318,7 @@ class TexturedSurface(Surface): super().init_uniforms() self.uniforms["num_textures"] = self.num_textures + @Mobject.affects_data def set_opacity(self, opacity: float | Iterable[float]): op_arr = np.array(listify(opacity)) self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data)) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index c04577ed..b6cecca9 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -233,6 +233,7 @@ class VMobject(Mobject): self.set_stroke(color, width, background=background) return self + @Mobject.affects_family_data def set_style( self, fill_color: ManimColor | Iterable[ManimColor] | None = None, @@ -1071,6 +1072,7 @@ class VMobject(Mobject): return self.data["joint_product"] self.needs_new_joint_products = False + self._data_has_changed = True points = self.get_points() @@ -1109,6 +1111,11 @@ class VMobject(Mobject): self.data["joint_product"][:, 3] = (vect_to_vert * vect_from_vert).sum(1) return self.data["joint_product"] + def lock_matching_data(self, vmobject1: VMobject, vmobject2: VMobject): + for mob in [self, vmobject1, vmobject2]: + mob.get_joint_products() + super().lock_matching_data(vmobject1, vmobject2) + def triggers_refreshed_triangulation(func: Callable): @wraps(func) def wrapper(self, *args, refresh=True, **kwargs): @@ -1119,10 +1126,11 @@ class VMobject(Mobject): return self return wrapper - @triggers_refreshed_triangulation def set_points(self, points: Vect3Array): assert(len(points) == 0 or len(points) % 2 == 1) super().set_points(points) + self.refresh_triangulation() + self.get_joint_products(refresh=True) return self @triggers_refreshed_triangulation diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 53d33d8d..04b4951c 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -189,7 +189,6 @@ class Scene(object): "Press `command + q` or `esc` to quit" ) self.skip_animations = False - self.refresh_static_mobjects() while not self.is_window_closing(): self.update_frame(1 / self.camera.fps) @@ -251,7 +250,6 @@ class Scene(object): # Operation to run after each ipython command def post_cell_func(): - self.refresh_static_mobjects() if not self.is_window_closing(): self.update_frame(dt=0, ignore_skipping=True) self.save_state() @@ -562,8 +560,6 @@ class Scene(object): self.real_animation_start_time = time.time() self.virtual_animation_start_time = self.time - self.refresh_static_mobjects() - def post_play(self): if not self.skip_animations: self.file_writer.end_animation() @@ -574,10 +570,6 @@ class Scene(object): self.num_plays += 1 - def refresh_static_mobjects(self) -> None: - for mobject in self.mobjects: - mobject._data_has_changed = True - def begin_animations(self, animations: Iterable[Animation]) -> None: for animation in animations: animation.begin() @@ -652,7 +644,6 @@ class Scene(object): self.emit_frame() if stop_condition is not None and stop_condition(): break - self.refresh_static_mobjects() self.post_play() def hold_loop(self): @@ -712,13 +703,11 @@ class Scene(object): if self.undo_stack: self.redo_stack.append(self.get_state()) self.restore_state(self.undo_stack.pop()) - self.refresh_static_mobjects() def redo(self): if self.redo_stack: self.undo_stack.append(self.get_state()) self.restore_state(self.redo_stack.pop()) - self.refresh_static_mobjects() def checkpoint_paste(self, skip: bool = False, record: bool = False): """ diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index b7e311b1..c273e765 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -51,16 +51,16 @@ class ShaderWrapper(object): self.depth_test = depth_test self.render_primitive = render_primitive - self.vbo = None - self.ibo = None - self.vao = None - self.init_program_code() self.init_program() if texture_paths is not None: self.init_textures(texture_paths) self.refresh_id() + self.vbo = None + self.ibo = None + self.vao = None + def init_program_code(self) -> None: def get_code(name: str) -> str | None: return get_shader_code_from_file( @@ -100,15 +100,16 @@ class ShaderWrapper(object): self.render_primitive == shader_wrapper.render_primitive, )) - def __del__(self): - self.release() - def copy(self): result = copy.copy(self) + result.ctx = self.ctx result.vert_data = self.vert_data.copy() result.vert_indices = self.vert_indices.copy() if self.uniforms: result.uniforms = {key: np.array(value) for key, value in self.uniforms.items()} + result.vao = None + result.vbo = None + result.ibo = None return result def is_valid(self) -> bool: @@ -219,7 +220,6 @@ class ShaderWrapper(object): self.update_program_uniforms() def render(self): - # TODO, generate on the fly? assert(self.vao is not None) self.vao.render() @@ -242,7 +242,8 @@ class ShaderWrapper(object): self.ibo = self.ctx.buffer(self.vert_indices.astype(np.uint32)) return self.ibo - def get_vao(self, refresh: bool = True): + def generate_vao(self, refresh: bool = True): + self.release() # Data buffer vbo = self.get_vertex_buffer_object(refresh) ibo = self.get_index_buffer_object(refresh) @@ -258,10 +259,7 @@ class ShaderWrapper(object): def release(self): for obj in (self.vbo, self.ibo, self.vao): if obj is not None: - try: - obj.release() - except AttributeError: - pass + obj.release() self.vbo = None self.ibo = None self.vao = None From 018b07212fec690b7e5c2617f22fd809c42c80d3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 16:50:52 -0800 Subject: [PATCH 12/37] Change how joint_products are updated in pointwise_become_partial --- manimlib/mobject/types/vectorized_mobject.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index b6cecca9..a5774b88 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -940,8 +940,9 @@ class VMobject(Mobject): def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float): assert(isinstance(vmobject, VMobject)) vm_points = vmobject.get_points() + self.data["joint_product"] = vmobject.data["joint_product"] if a <= 0 and b >= 1: - self.set_points(vm_points) + self.set_points(vm_points, refresh_joints=False) return self num_curves = vmobject.get_num_curves() @@ -974,7 +975,9 @@ class VMobject(Mobject): # Keep new_points i2:i3 as they are new_points[i3:i4] = high_tup new_points[i4:] = high_tup[2] - self.set_points(new_points) + self.data["joint_product"][:i1] = [0, 0, 0, 1] + self.data["joint_product"][i4:] = [0, 0, 0, 1] + self.set_points(new_points, refresh_joints=False) return self def get_subcurve(self, a: float, b: float) -> VMobject: @@ -1126,11 +1129,12 @@ class VMobject(Mobject): return self return wrapper - def set_points(self, points: Vect3Array): + def set_points(self, points: Vect3Array, refresh_joints: bool = True): assert(len(points) == 0 or len(points) % 2 == 1) super().set_points(points) self.refresh_triangulation() - self.get_joint_products(refresh=True) + if refresh_joints: + self.get_joint_products(refresh=True) return self @triggers_refreshed_triangulation From 7609b1db783aff558a6bf89338b98dbd6751b511 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 17:19:44 -0800 Subject: [PATCH 13/37] Set up by-hand anti-aliasing for FillShaderWrapper --- manimlib/shader_wrapper.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index c273e765..b8c9c2a8 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -275,17 +275,17 @@ class FillShaderWrapper(ShaderWrapper): super().__init__(ctx, *args, **kwargs) size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) - self.fill_texture = ctx.texture( + texture = ctx.texture( size=size, components=4, # Important to make sure floating point (not fixed point) is # used so that alpha values are not clipped dtype='f2', ) - # TODO, depth buffer is not really used yet - fill_depth = ctx.depth_renderbuffer(size) - self.fill_fbo = ctx.framebuffer(self.fill_texture, fill_depth) - self.fill_prog = ctx.program( + depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used + self.texture_fbo = ctx.framebuffer(texture, depth_buffer) + + simple_program = ctx.program( vertex_shader=''' #version 330 @@ -301,24 +301,38 @@ class FillShaderWrapper(ShaderWrapper): #version 330 uniform sampler2D Texture; + uniform sampler2D DepthTexture; + uniform float v_nudge; + uniform float h_nudge; in vec2 v_textcoord; out vec4 frag_color; void main() { - frag_color = texture(Texture, v_textcoord); - frag_color = abs(frag_color); + // Apply poor man's anti-aliasing + vec2 tc0 = v_textcoord + vec2(v_nudge, h_nudge); + vec2 tc1 = v_textcoord + vec2(v_nudge, -h_nudge); + vec2 tc2 = v_textcoord + vec2(-v_nudge, h_nudge); + vec2 tc3 = v_textcoord + vec2(-v_nudge, -h_nudge); + frag_color = + 0.25 * abs(texture(Texture, tc0)) + + 0.25 * abs(texture(Texture, tc1)) + + 0.25 * abs(texture(Texture, tc2)) + + 0.25 * abs(texture(Texture, tc3)); if(frag_color.a == 0) discard; //TODO, set gl_FragDepth; } ''', ) - self.fill_prog['Texture'].value = get_texture_id(self.fill_texture) + simple_program['Texture'].value = get_texture_id(texture) + # Quarter pixel width/height + simple_program['h_nudge'].value = 0.25 / size[0] + simple_program['v_nudge'].value = 0.25 / size[1] verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) self.fill_texture_vao = ctx.simple_vertex_array( - self.fill_prog, + simple_program, ctx.buffer(verts.astype('f4').tobytes()), 'texcoord', ) @@ -332,8 +346,8 @@ class FillShaderWrapper(ShaderWrapper): vao.render(moderngl.TRIANGLES) return original_fbo = self.ctx.fbo - self.fill_fbo.clear() - self.fill_fbo.use() + self.texture_fbo.clear() + self.texture_fbo.use() self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) vao.render(self.render_primitive) self.ctx.blend_func = moderngl.DEFAULT_BLENDING From 2beb55727fd2387631b87c9d0bbf48e058e26928 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 17:30:12 -0800 Subject: [PATCH 14/37] Change naming logic for recorded inserts --- manimlib/scene/scene_file_writer.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index d17f1dbd..d288e7dd 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -293,13 +293,10 @@ class SceneFileWriter(object): self.write_to_movie = True self.init_output_directories() movie_path = self.get_movie_file_path() - folder, file = os.path.split(movie_path) - scene_name, ext = file.split(".") - n_inserts = len(list(filter( - lambda f: f.startswith(scene_name + "_insert"), - os.listdir(folder) - ))) - self.inserted_file_path = movie_path.replace(".", f"_insert_{n_inserts}.") + count = 0 + while os.path.exists(name := movie_path.replace(".", f"_insert_{count}.")): + count += 1 + self.inserted_file_path = name self.open_movie_pipe(self.inserted_file_path) def end_insert(self): From 3c8e3792e7d45fe7c5f8f8a48f4f59af28d2be1f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 19:20:11 -0800 Subject: [PATCH 15/37] Remove references to refresh_static_mobjects --- manimlib/scene/interactive_scene.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 80c4c548..fe794bc3 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -317,14 +317,12 @@ class InteractiveScene(Scene): mob.refresh_bounding_box() else: self.add_to_selection(mob) - self.refresh_static_mobjects() def clear_selection(self): for mob in self.selection: mob.set_animating_status(False) mob.refresh_bounding_box() self.selection.set_submobjects([]) - self.refresh_static_mobjects() def disable_interaction(self, *mobjects: Mobject): for mob in mobjects: From f96a697ee3bccf67e436375794303ebd031609bd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 19:21:24 -0800 Subject: [PATCH 16/37] Use become for interpolating at 0 or 1 --- manimlib/animation/transform.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 9c07747d..555a85b3 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -121,6 +121,10 @@ class Transform(Animation): target_copy: Mobject, alpha: float ): + if alpha == 0: + submob.become(start) + elif alpha == 1: + submob.become(target_copy) submob.interpolate(start, target_copy, alpha, self.path_func) return self From a601384211435b0538c286def3deaa11a08bb0f8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 19:23:22 -0800 Subject: [PATCH 17/37] Remove stray imports --- manimlib/camera/camera.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index fa65c6cd..55c0c948 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -1,7 +1,5 @@ from __future__ import annotations -import itertools as it - import moderngl import numpy as np import OpenGL.GL as gl @@ -15,15 +13,12 @@ from manimlib.constants import FRAME_WIDTH from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point from manimlib.utils.color import color_to_rgba -from manimlib.utils.shaders import get_texture_id from typing import TYPE_CHECKING if TYPE_CHECKING: - from manimlib.shader_wrapper import ShaderWrapper from manimlib.typing import ManimColor, Vect3 from manimlib.window import Window - from typing import Any, Iterable class Camera(object): From 88590e5a057a3aab34a40e2938bf82affda13775 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 19:23:55 -0800 Subject: [PATCH 18/37] Remove serializing deepcopy --- manimlib/mobject/mobject.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 5c1b1576..f8660928 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -587,11 +587,9 @@ class Mobject(object): return self def deepcopy(self): - try: - # Often faster than deepcopy - return pickle.loads(pickle.dumps(self)) - except AttributeError: - return copy.deepcopy(self) + result = copy.deepcopy(self) + result._shaders_initialized = False + result._data_has_changed = True @stash_mobject_pointers def copy(self, deep: bool = False): From cb36fda6d7c93d8cb9e5df304e70a0bd51ac6e42 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 19:24:19 -0800 Subject: [PATCH 19/37] In interpolate, only update data status if some keys are unlocked --- manimlib/mobject/mobject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index f8660928..bf82f33f 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1710,7 +1710,6 @@ class Mobject(object): # Interpolate - @affects_data def interpolate( self, mobject1: Mobject, @@ -1719,6 +1718,8 @@ class Mobject(object): path_func: Callable[[np.ndarray, np.ndarray, float], np.ndarray] = straight_path ): keys = [k for k in self.data.dtype.names if k not in self.locked_data_keys] + if keys: + self.note_changed_data() for key in keys: func = path_func if key in self.pointlike_data_keys else interpolate md1 = mobject1.data[key] From a68bc1271b87d8a3ba44470a89b4fd26d4dbee07 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 19:43:16 -0800 Subject: [PATCH 20/37] Have FillShaders all share the same texture used for intermediary rendering --- manimlib/shader_wrapper.py | 75 ++++-------------------------------- manimlib/utils/shaders.py | 78 +++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 68 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index b8c9c2a8..a384be01 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -7,14 +7,14 @@ import re import OpenGL.GL as gl import moderngl import numpy as np +from functools import lru_cache -from manimlib.constants import DEFAULT_PIXEL_HEIGHT -from manimlib.constants import DEFAULT_PIXEL_WIDTH from manimlib.utils.iterables import resize_array from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_program from manimlib.utils.shaders import image_path_to_texture from manimlib.utils.shaders import get_texture_id +from manimlib.utils.shaders import get_intermediary_palette from manimlib.utils.shaders import release_texture from typing import TYPE_CHECKING @@ -274,68 +274,6 @@ class FillShaderWrapper(ShaderWrapper): ): super().__init__(ctx, *args, **kwargs) - size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) - texture = ctx.texture( - size=size, - components=4, - # Important to make sure floating point (not fixed point) is - # used so that alpha values are not clipped - dtype='f2', - ) - depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used - self.texture_fbo = ctx.framebuffer(texture, depth_buffer) - - simple_program = ctx.program( - vertex_shader=''' - #version 330 - - in vec2 texcoord; - out vec2 v_textcoord; - - void main() { - gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0); - v_textcoord = texcoord; - } - ''', - fragment_shader=''' - #version 330 - - uniform sampler2D Texture; - uniform sampler2D DepthTexture; - uniform float v_nudge; - uniform float h_nudge; - - in vec2 v_textcoord; - out vec4 frag_color; - - void main() { - // Apply poor man's anti-aliasing - vec2 tc0 = v_textcoord + vec2(v_nudge, h_nudge); - vec2 tc1 = v_textcoord + vec2(v_nudge, -h_nudge); - vec2 tc2 = v_textcoord + vec2(-v_nudge, h_nudge); - vec2 tc3 = v_textcoord + vec2(-v_nudge, -h_nudge); - frag_color = - 0.25 * abs(texture(Texture, tc0)) + - 0.25 * abs(texture(Texture, tc1)) + - 0.25 * abs(texture(Texture, tc2)) + - 0.25 * abs(texture(Texture, tc3)); - if(frag_color.a == 0) discard; - //TODO, set gl_FragDepth; - } - ''', - ) - - simple_program['Texture'].value = get_texture_id(texture) - # Quarter pixel width/height - simple_program['h_nudge'].value = 0.25 / size[0] - simple_program['v_nudge'].value = 0.25 / size[1] - - verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) - self.fill_texture_vao = ctx.simple_vertex_array( - simple_program, - ctx.buffer(verts.astype('f4').tobytes()), - 'texcoord', - ) def render(self): vao = self.vao @@ -345,11 +283,14 @@ class FillShaderWrapper(ShaderWrapper): if not winding: vao.render(moderngl.TRIANGLES) return + + texture_fbo, texture_vao = get_intermediary_palette(self.ctx) + original_fbo = self.ctx.fbo - self.texture_fbo.clear() - self.texture_fbo.use() + texture_fbo.clear() + texture_fbo.use() self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) vao.render(self.render_primitive) self.ctx.blend_func = moderngl.DEFAULT_BLENDING original_fbo.use() - self.fill_texture_vao.render(moderngl.TRIANGLE_STRIP) + texture_vao.render(moderngl.TRIANGLE_STRIP) diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index eaddcaea..4729126f 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -6,13 +6,17 @@ from functools import lru_cache import moderngl from PIL import Image +from manimlib.constants import DEFAULT_PIXEL_HEIGHT +from manimlib.constants import DEFAULT_PIXEL_WIDTH from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Sequence, Optional + from typing import Sequence, Optional, Tuple + from moderngl.vertex_array import VertexArray + from moderngl.framebuffer import Framebuffer ID_TO_TEXTURE: dict[int, moderngl.Texture] = dict() @@ -93,3 +97,75 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str: for rgb in rgb_list ) return f"vec3[{len(rgb_list)}]({data})" + + + +@lru_cache() +def get_intermediary_palette(ctx) -> Tuple[Framebuffer, VertexArray]: + """ + Creates a texture, loaded into a frame buffer, and a vao + which can display that texture as a simple quad onto a screen. + """ + size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) + texture = ctx.texture( + size=size, + components=4, + # Important to make sure floating point (not fixed point) is + # used so that alpha values are not clipped + dtype='f2', + ) + depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used + texture_fbo = ctx.framebuffer(texture, depth_buffer) + + simple_program = ctx.program( + vertex_shader=''' + #version 330 + + in vec2 texcoord; + out vec2 v_textcoord; + + void main() { + gl_Position = vec4((2.0 * texcoord - 1.0), 0.0, 1.0); + v_textcoord = texcoord; + } + ''', + fragment_shader=''' + #version 330 + + uniform sampler2D Texture; + uniform sampler2D DepthTexture; + uniform float v_nudge; + uniform float h_nudge; + + in vec2 v_textcoord; + out vec4 frag_color; + + void main() { + // Apply poor man's anti-aliasing + vec2 tc0 = v_textcoord + vec2(v_nudge, h_nudge); + vec2 tc1 = v_textcoord + vec2(v_nudge, -h_nudge); + vec2 tc2 = v_textcoord + vec2(-v_nudge, h_nudge); + vec2 tc3 = v_textcoord + vec2(-v_nudge, -h_nudge); + frag_color = + 0.25 * abs(texture(Texture, tc0)) + + 0.25 * abs(texture(Texture, tc1)) + + 0.25 * abs(texture(Texture, tc2)) + + 0.25 * abs(texture(Texture, tc3)); + if(frag_color.a == 0) discard; + //TODO, set gl_FragDepth; + } + ''', + ) + + simple_program['Texture'].value = get_texture_id(texture) + # Quarter pixel width/height + simple_program['h_nudge'].value = 0.25 / size[0] + simple_program['v_nudge'].value = 0.25 / size[1] + + verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) + fill_texture_vao = ctx.simple_vertex_array( + simple_program, + ctx.buffer(verts.astype('f4').tobytes()), + 'texcoord', + ) + return (texture_fbo, fill_texture_vao) From a1595a9e2f2b119300da23a2ad0104f7d3ec3bf9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jan 2023 22:34:11 -0800 Subject: [PATCH 21/37] Use separate fbo for writing to file when window is active --- manimlib/camera/camera.py | 19 ++++++++++++++----- manimlib/config.py | 5 +---- manimlib/scene/scene.py | 8 ++------ manimlib/window.py | 4 +--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 55c0c948..f9906e9e 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -69,12 +69,16 @@ class Camera(object): self.frame = CameraFrame(**config) def init_context(self, window: Window | None = None) -> None: + self.window = window if window is None: self.ctx = moderngl.create_standalone_context() self.fbo = self.get_fbo(self.samples) else: self.ctx = window.ctx - self.fbo = self.ctx.detect_framebuffer() + self.window_fbo = self.ctx.detect_framebuffer() + self.fbo_for_files = self.get_fbo(self.samples) + self.fbo = self.window_fbo + self.fbo.use() self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) @@ -86,6 +90,13 @@ class Camera(object): def init_light_source(self) -> None: self.light_source = Point(self.light_source_position) + def use_window_fbo(self, use: bool = True): + assert(self.window is not None) + if use: + self.fbo = self.window_fbo + else: + self.fbo = self.fbo_for_files + # Methods associated with the frame buffer def get_fbo( self, @@ -110,10 +121,7 @@ class Camera(object): # Copy blocks from fbo into draw_fbo using Blit gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo) gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo) - if self.window is not None: - src_viewport = self.window.viewport - else: - src_viewport = self.fbo.viewport + src_viewport = self.fbo.viewport gl.glBlitFramebuffer( *src_viewport, *self.draw_fbo.viewport, @@ -203,6 +211,7 @@ class Camera(object): # Rendering def capture(self, *mobjects: Mobject) -> None: self.refresh_uniforms() + self.fbo.use() for mobject in mobjects: mobject.render(self.ctx, self.uniforms) diff --git a/manimlib/config.py b/manimlib/config.py index 26ddb708..e3e93420 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -414,10 +414,7 @@ def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) if not (args.full_screen or custom_config["full_screen"]): window_width //= 2 window_height = int(window_width / aspect_ratio) - return dict( - full_size=(camera_config["pixel_width"], camera_config["pixel_height"]), - size=(window_width, window_height), - ) + return dict(size=(window_width, window_height)) def get_camera_config(args: Namespace, custom_config: dict) -> dict: diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 04b4951c..4383eb7c 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -737,18 +737,14 @@ class Scene(object): self.skip_animations = skip if record: - # Resize window so rendering happens at the appropriate size - self.window.size = self.camera.get_pixel_shape() - self.window.swap_buffers() - self.update_frame() + self.camera.use_window_fbo(False) self.file_writer.begin_insert() shell.run_cell(pasted) if record: self.file_writer.end_insert() - # Put window back to how it started - self.window.to_default_position() + self.camera.use_window_fbo(True) self.skip_animations = prev_skipping diff --git a/manimlib/window.py b/manimlib/window.py index 93337654..8006b382 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -26,12 +26,10 @@ class Window(PygletWindow): self, scene: Scene, size: tuple[int, int] = (1280, 720), - full_size: tuple[int, int] = (1920, 1080), samples = 0 ): - super().__init__(size=full_size, samples=samples) + super().__init__(size=size, samples=samples) - self.full_size = full_size self.default_size = size self.default_position = self.find_initial_position(size) self.scene = scene From 72590a8fefb6868eb55e03fbc9a3ce01f935fe2e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 12:04:34 -0800 Subject: [PATCH 22/37] Note that using winding fill changes data --- manimlib/mobject/types/vectorized_mobject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a5774b88..d597e057 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -417,6 +417,7 @@ class VMobject(Mobject): def get_joint_type(self) -> float: return self.uniforms["joint_type"] + @Mobject.affects_family_data def use_winding_fill(self, value: bool = True, recurse: bool = True): for submob in self.get_family(recurse): submob._use_winding_fill = value @@ -835,7 +836,7 @@ class VMobject(Mobject): # If both have fill, and they have the same shape, just # give them the same triangulation so that it's not recalculated # needlessly throughout an animation - if self._use_winding_fill and self.has_fill() \ + if not self._use_winding_fill and self.has_fill() \ and vmobject.has_fill() and self.has_same_shape_as(vmobject): vmobject.triangulation = self.triangulation return self From 05dd3992707508de54a7a52cae4e1f9fc07497cc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 12:04:58 -0800 Subject: [PATCH 23/37] Ensure svgs have positive orientation by default --- manimlib/mobject/svg/svg_mobject.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index 6ca56596..d140dd19 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -320,8 +320,9 @@ class VMobjectFromSVGPath(VMobject): self.set_points(self.get_points_without_null_curves()) # So triangulation doesn't get messed up self.subdivide_intersections() - # Always default to orienting outward - if self.get_unit_normal()[2] < 0: + # Always default to orienting outward, account + # for the fact that this will get flipped in SVG.__init__ + if self.get_unit_normal()[2] > 0: self.reverse_points() # Save for future use PATH_TO_POINTS[path_string] = self.get_points().copy() From f5cb2bfa52b0d6581a3a66cfcca740e02b1dca91 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 12:06:05 -0800 Subject: [PATCH 24/37] Check for mismatched keys in uniform interpolation --- manimlib/mobject/mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index bf82f33f..b002f26b 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1730,6 +1730,8 @@ class Mobject(object): self.data[key] = func(md1, md2, alpha) for key in self.uniforms: + if key not in mobject1.uniforms or key not in mobject2.uniforms: + continue self.uniforms[key] = interpolate( mobject1.uniforms[key], mobject2.uniforms[key], From c6c23a1fe79cc15b19159d09ec8d06361bef034b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 12:06:13 -0800 Subject: [PATCH 25/37] Unnecessary refresh_bounding_box --- manimlib/mobject/mobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index b002f26b..2d6139d2 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -683,7 +683,6 @@ class Mobject(object): for attr, value in list(mobject.__dict__.items()): if isinstance(value, Mobject) and value in family2: setattr(self, attr, family1[family2.index(value)]) - self.refresh_bounding_box(recurse_down=True) if match_updaters: self.match_updaters(mobject) return self From a33b24310e3b33ab42a5c2e5bbe252dccd1c0d3d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 12:15:30 -0800 Subject: [PATCH 26/37] Fix non-winding-fill orientation --- manimlib/mobject/types/vectorized_mobject.py | 6 ++++-- manimlib/shaders/quadratic_bezier_fill/geom.glsl | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index d597e057..79b04bf6 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1053,8 +1053,10 @@ class VMobject(Mobject): null2 = (iti[0::3] - 1 == iti[1::3]) & (iti[0::3] - 2 == iti[2::3]) inner_tri_indices = iti[~(null1 | null2).repeat(3)] - outer_tri_indices = self.get_outer_vert_indices() - tri_indices = np.hstack([outer_tri_indices, inner_tri_indices]) + ovi = self.get_outer_vert_indices() + # Flip outer triangles with negative orientation + ovi[0::3][concave_parts], ovi[2::3][concave_parts] = ovi[2::3][concave_parts], ovi[0::3][concave_parts] + tri_indices = np.hstack([ovi, inner_tri_indices]) self.triangulation = tri_indices self.needs_new_triangulation = False return tri_indices diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index fae7998b..1326c365 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -31,7 +31,7 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3]( void emit_triangle(vec3 points[3], vec4 v_color[3]){ vec3 unit_normal = get_unit_normal(points[0], points[1], points[2]); - orientation = sign(unit_normal.z); + orientation = winding ? sign(unit_normal.z) : 1.0; for(int i = 0; i < 3; i++){ uv_coords = SIMPLE_QUADRATIC[i]; From 3f5df432ce159f2ceac9d295bed20fa7a6257dbe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 12:17:21 -0800 Subject: [PATCH 27/37] Consider winding_fill alphas pre-multiplied --- manimlib/shader_wrapper.py | 10 +++-- .../shaders/quadratic_bezier_fill/frag.glsl | 9 ++++- manimlib/utils/shaders.py | 37 +++++++++---------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index a384be01..da042544 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -14,7 +14,7 @@ from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_program from manimlib.utils.shaders import image_path_to_texture from manimlib.utils.shaders import get_texture_id -from manimlib.utils.shaders import get_intermediary_palette +from manimlib.utils.shaders import get_fill_palette from manimlib.utils.shaders import release_texture from typing import TYPE_CHECKING @@ -284,13 +284,15 @@ class FillShaderWrapper(ShaderWrapper): vao.render(moderngl.TRIANGLES) return - texture_fbo, texture_vao = get_intermediary_palette(self.ctx) - original_fbo = self.ctx.fbo + texture_fbo, texture_vao = get_fill_palette(self.ctx) + texture_fbo.clear() texture_fbo.use() self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) vao.render(self.render_primitive) - self.ctx.blend_func = moderngl.DEFAULT_BLENDING + + self.ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA) original_fbo.use() texture_vao.render(moderngl.TRIANGLE_STRIP) + self.ctx.blend_func = moderngl.DEFAULT_BLENDING diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index c672455a..ae17b366 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -13,13 +13,18 @@ void main() { if (color.a == 0) discard; frag_color = color; - if(winding && orientation > 0) frag_color *= -1; + // Pre-multiply alphas + if(winding) frag_color *= frag_color.a; + + // Give a sign based on orientation so that + // additive blending cancels as needed + if(winding && orientation < 0) frag_color *= -1; if (bool(fill_all)) return; float x = uv_coords.x; float y = uv_coords.y; float Fxy = (y - x * x); - if(!winding && orientation > 0) Fxy *= -1; + if(!winding && orientation < 0) Fxy *= -1; if(Fxy < 0) discard; } diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 4729126f..8436fc8a 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -5,6 +5,7 @@ import re from functools import lru_cache import moderngl from PIL import Image +import numpy as np from manimlib.constants import DEFAULT_PIXEL_HEIGHT from manimlib.constants import DEFAULT_PIXEL_WIDTH @@ -101,19 +102,15 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str: @lru_cache() -def get_intermediary_palette(ctx) -> Tuple[Framebuffer, VertexArray]: +def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: """ Creates a texture, loaded into a frame buffer, and a vao which can display that texture as a simple quad onto a screen. """ size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) - texture = ctx.texture( - size=size, - components=4, - # Important to make sure floating point (not fixed point) is - # used so that alpha values are not clipped - dtype='f2', - ) + # Important to make sure dtype is floating point (not fixed point) + # so that alpha values can be negative and are not clipped + texture = ctx.texture(size=size, components=4, dtype='f2') depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used texture_fbo = ctx.framebuffer(texture, depth_buffer) @@ -133,7 +130,6 @@ def get_intermediary_palette(ctx) -> Tuple[Framebuffer, VertexArray]: #version 330 uniform sampler2D Texture; - uniform sampler2D DepthTexture; uniform float v_nudge; uniform float h_nudge; @@ -142,25 +138,26 @@ def get_intermediary_palette(ctx) -> Tuple[Framebuffer, VertexArray]: void main() { // Apply poor man's anti-aliasing - vec2 tc0 = v_textcoord + vec2(v_nudge, h_nudge); - vec2 tc1 = v_textcoord + vec2(v_nudge, -h_nudge); - vec2 tc2 = v_textcoord + vec2(-v_nudge, h_nudge); - vec2 tc3 = v_textcoord + vec2(-v_nudge, -h_nudge); + vec2 tc0 = v_textcoord + vec2(0, 0); + vec2 tc1 = v_textcoord + vec2(0, h_nudge); + vec2 tc2 = v_textcoord + vec2(v_nudge, 0); + vec2 tc3 = v_textcoord + vec2(v_nudge, h_nudge); frag_color = - 0.25 * abs(texture(Texture, tc0)) + - 0.25 * abs(texture(Texture, tc1)) + - 0.25 * abs(texture(Texture, tc2)) + - 0.25 * abs(texture(Texture, tc3)); + 0.25 * texture(Texture, tc0) + + 0.25 * texture(Texture, tc1) + + 0.25 * texture(Texture, tc2) + + 0.25 * texture(Texture, tc3); if(frag_color.a == 0) discard; + frag_color = abs(frag_color); //TODO, set gl_FragDepth; } ''', ) simple_program['Texture'].value = get_texture_id(texture) - # Quarter pixel width/height - simple_program['h_nudge'].value = 0.25 / size[0] - simple_program['v_nudge'].value = 0.25 / size[1] + # Half pixel width/height + simple_program['h_nudge'].value = 0.5 / size[0] + simple_program['v_nudge'].value = 0.5 / size[1] verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) fill_texture_vao = ctx.simple_vertex_array( From 37f0bf8c11a556a5462b1e10b3f53ea4d73494db Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 15:27:48 -0800 Subject: [PATCH 28/37] Fix winding fill blending (Using somewhat of a hack) --- manimlib/shader_wrapper.py | 7 +++--- .../shaders/quadratic_bezier_fill/frag.glsl | 24 +++++++++++++++---- .../shaders/quadratic_bezier_fill/geom.glsl | 5 ++++ manimlib/utils/shaders.py | 5 ++-- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index da042544..aa733f8e 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -289,10 +289,9 @@ class FillShaderWrapper(ShaderWrapper): texture_fbo.clear() texture_fbo.use() - self.ctx.blend_func = (moderngl.ONE, moderngl.ONE) - vao.render(self.render_primitive) + vao.render() - self.ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA) original_fbo.use() + self.ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA) texture_vao.render(moderngl.TRIANGLE_STRIP) - self.ctx.blend_func = moderngl.DEFAULT_BLENDING + self.ctx.blend_func = (moderngl.DEFAULT_BLENDING) diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index ae17b366..94fc495a 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -13,12 +13,26 @@ void main() { if (color.a == 0) discard; frag_color = color; - // Pre-multiply alphas - if(winding) frag_color *= frag_color.a; + /* + We want negatively oriented triangles to be canceled with positively + oriented ones. The easiest way to do this is to give them negative alpha, + and change the blend function to just add them. However, this messes with + usual blending, so instead the following line is meant to let this canceling + work even for the normal blending equation: - // Give a sign based on orientation so that - // additive blending cancels as needed - if(winding && orientation < 0) frag_color *= -1; + (1 - alpha) * dst + alpha * src + + We want the effect of blending with a positively oriented triangle followed + by a negatively oriented one to return to whatever the original frag value + was. You can work out this will work if the alpha for negative orientations + is changed to -alpha / (1 - alpha). This has a singularity at alpha = 1, + so we cap it at a value very close to 1. Effectively, the purpose of this + cap is to make sure the original fragment color can be recovered even after + blending with an alpha = 1 color. + */ + float a = 0.999 * frag_color.a; + if(winding && orientation < 0) a = -a / (1 - a); + frag_color.a = a; if (bool(fill_all)) return; diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 1326c365..284be5e6 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -36,6 +36,11 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){ for(int i = 0; i < 3; i++){ uv_coords = SIMPLE_QUADRATIC[i]; color = finalize_color(v_color[i], points[i], unit_normal); + if(winding){ + // Pure black will be used to discard fragments later + if(color.rgb == vec3(0.0)) color.rgb += vec3(0.01); + // color.a = sqrt(color.a); + } gl_Position = get_gl_Position(points[i]); EmitVertex(); } diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 8436fc8a..1556015b 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -110,7 +110,7 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT) # Important to make sure dtype is floating point (not fixed point) # so that alpha values can be negative and are not clipped - texture = ctx.texture(size=size, components=4, dtype='f2') + texture = ctx.texture(size=size, components=4, dtype='f4') depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used texture_fbo = ctx.framebuffer(texture, depth_buffer) @@ -147,8 +147,7 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: 0.25 * texture(Texture, tc1) + 0.25 * texture(Texture, tc2) + 0.25 * texture(Texture, tc3); - if(frag_color.a == 0) discard; - frag_color = abs(frag_color); + if(distance(frag_color.rgb, vec3(0.0)) < 1e-3) discard; //TODO, set gl_FragDepth; } ''', From 14cda7e90857fadd1e8ac5107b2f7e442d3b1fda Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 15:28:10 -0800 Subject: [PATCH 29/37] Don't show progress bar in embed by default --- manimlib/scene/scene.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 4383eb7c..3d933970 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -195,7 +195,7 @@ class Scene(object): def embed( self, close_scene_on_exit: bool = True, - show_animation_progress: bool = True, + show_animation_progress: bool = False, ) -> None: if not self.preview: return # Embed is only relevant with a preview @@ -709,7 +709,12 @@ class Scene(object): self.undo_stack.append(self.get_state()) self.restore_state(self.redo_stack.pop()) - def checkpoint_paste(self, skip: bool = False, record: bool = False): + def checkpoint_paste( + self, + skip: bool = False, + record: bool = False, + progress_bar: bool = True + ): """ Used during interactive development to run (or re-run) a block of scene code. @@ -736,6 +741,9 @@ class Scene(object): prev_skipping = self.skip_animations self.skip_animations = skip + prev_progress = self.show_animation_progress + self.show_animation_progress = progress_bar + if record: self.camera.use_window_fbo(False) self.file_writer.begin_insert() @@ -747,6 +755,7 @@ class Scene(object): self.camera.use_window_fbo(True) self.skip_animations = prev_skipping + self.show_animation_progress = prev_progress def checkpoint(self, key: str): self.checkpoint_states[key] = self.get_state() From 65afed1bd172513582994f8c9fff84d7d2ea8b58 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 15:50:27 -0800 Subject: [PATCH 30/37] Move shading from fill geom to fill frag shader --- manimlib/shaders/quadratic_bezier_fill/frag.glsl | 6 ++++-- manimlib/shaders/quadratic_bezier_fill/geom.glsl | 12 +++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index 94fc495a..352ad949 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -6,13 +6,15 @@ in vec4 color; in float fill_all; in float orientation; in vec2 uv_coords; +in vec3 point; out vec4 frag_color; +#INSERT finalize_color.glsl + void main() { if (color.a == 0) discard; - frag_color = color; - + frag_color = finalize_color(color, point, vec3(0.0, 0.0, 1.0)); /* We want negatively oriented triangles to be canceled with positively oriented ones. The easiest way to do this is to give them negative alpha, diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 284be5e6..242f8621 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -13,6 +13,7 @@ in float v_vert_index[3]; out vec4 color; out float fill_all; out float orientation; +out vec3 point; // uv space is where the curve coincides with y = x^2 out vec2 uv_coords; @@ -26,7 +27,6 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3]( // Analog of import for manim only #INSERT get_gl_Position.glsl #INSERT get_unit_normal.glsl -#INSERT finalize_color.glsl void emit_triangle(vec3 points[3], vec4 v_color[3]){ @@ -35,12 +35,10 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){ for(int i = 0; i < 3; i++){ uv_coords = SIMPLE_QUADRATIC[i]; - color = finalize_color(v_color[i], points[i], unit_normal); - if(winding){ - // Pure black will be used to discard fragments later - if(color.rgb == vec3(0.0)) color.rgb += vec3(0.01); - // color.a = sqrt(color.a); - } + color = v_color[i]; + point = points[i]; + // Pure black will be used to discard fragments later + if(winding && color.rgb == vec3(0.0)) color.rgb += vec3(0.01); gl_Position = get_gl_Position(points[i]); EmitVertex(); } From a8da171adbefc9e3b13d989382d5c68f1ff0223c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 15:58:56 -0800 Subject: [PATCH 31/37] Make sure a group inherits the fixed_in_frame status of its parts --- manimlib/mobject/mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 2d6139d2..9c9faf36 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -2048,6 +2048,8 @@ class Group(Mobject): raise Exception("All submobjects must be of type Mobject") Mobject.__init__(self, **kwargs) self.add(*mobjects) + if any(m.is_fixed_in_frame for m in mobjects): + self.fix_in_frame() def __add__(self, other: Mobject | Group): assert(isinstance(other, Mobject)) From 8ef71bb9304425bdc7844e41fb7bba61d21752bb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 16:43:40 -0800 Subject: [PATCH 32/37] Don't use 'become' for interpolate at alpha = 0 or 1 --- manimlib/animation/transform.py | 4 ---- manimlib/mobject/mobject.py | 6 ++++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 555a85b3..9c07747d 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -121,10 +121,6 @@ class Transform(Animation): target_copy: Mobject, alpha: float ): - if alpha == 0: - submob.become(start) - elif alpha == 1: - submob.become(target_copy) submob.interpolate(start, target_copy, alpha, self.path_func) return self diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 9c9faf36..872feda5 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1719,6 +1719,12 @@ class Mobject(object): keys = [k for k in self.data.dtype.names if k not in self.locked_data_keys] if keys: self.note_changed_data() + if alpha == 0: + self.data[:] = mobject1.data[:] + keys = [] + elif alpha == 1: + self.data[:] = mobject2.data[:] + keys = [] for key in keys: func = path_func if key in self.pointlike_data_keys else interpolate md1 = mobject1.data[key] From 164c9ba75491b6706acffc2a8ad11ae27214e5a7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 16:46:05 -0800 Subject: [PATCH 33/37] Use copy in set_data --- manimlib/mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 872feda5..7db253b2 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -181,7 +181,7 @@ class Mobject(object): @affects_data def set_data(self, data: np.ndarray): assert(data.dtype == self.data.dtype) - self.data = data + self.data = data.copy() return self @affects_data From 28c875c2c32adf5a5a742f11be2412d9815cfd26 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 16:49:13 -0800 Subject: [PATCH 34/37] Finish Transforms with a call to Mobject.become --- manimlib/animation/transform.py | 1 + manimlib/mobject/mobject.py | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 9c07747d..62fd87a7 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -70,6 +70,7 @@ class Transform(Animation): def finish(self) -> None: super().finish() self.mobject.unlock_data() + self.mobject.become(self.target_mobject) def create_target(self) -> Mobject: # Has no meaningful effect here, but may be useful diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 7db253b2..313517ee 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1719,12 +1719,6 @@ class Mobject(object): keys = [k for k in self.data.dtype.names if k not in self.locked_data_keys] if keys: self.note_changed_data() - if alpha == 0: - self.data[:] = mobject1.data[:] - keys = [] - elif alpha == 1: - self.data[:] = mobject2.data[:] - keys = [] for key in keys: func = path_func if key in self.pointlike_data_keys else interpolate md1 = mobject1.data[key] From 6d4782506af1e5a916f50d5465c19c29323dbabc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 16:49:36 -0800 Subject: [PATCH 35/37] Account for null fill cases in invinisble_copy --- manimlib/mobject/types/vectorized_mobject.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 79b04bf6..112dc9ce 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -885,6 +885,8 @@ class VMobject(Mobject): def invisible_copy(self): result = self.copy() + if not result.has_fill() or result.get_num_points() == 0: + return result result.append_vectorized_mobject(self.copy().reverse_points()) result.set_opacity(0) return result From 96968272139514563873475cee2cf11f5f6a45f6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 16:50:22 -0800 Subject: [PATCH 36/37] Allow for adding null subpath --- manimlib/mobject/types/vectorized_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 112dc9ce..e83c76e3 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -658,7 +658,7 @@ class VMobject(Mobject): return self def add_subpath(self, points: Vect3Array): - assert(len(points) % 2 == 1) + assert(len(points) % 2 == 1 or len(points) == 0) if not self.has_points(): self.set_points(points) return self From acdc2654d350a4e88f05d4cff98e519c729f1829 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 16:52:25 -0800 Subject: [PATCH 37/37] Account for 'target_mobject is None' case --- manimlib/animation/transform.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 62fd87a7..012bfad9 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -70,7 +70,8 @@ class Transform(Animation): def finish(self) -> None: super().finish() self.mobject.unlock_data() - self.mobject.become(self.target_mobject) + if self.target_mobject is not None: + self.mobject.become(self.target_mobject) def create_target(self) -> Mobject: # Has no meaningful effect here, but may be useful