From 258bc2256ae5043c55b2b38e0202c14cb7fc47ee Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:01:59 -0800 Subject: [PATCH 01/36] Provide a check that shader uniforms really need updating before setting value --- manimlib/mobject/mobject.py | 10 +++---- manimlib/mobject/types/vectorized_mobject.py | 5 ---- manimlib/shader_wrapper.py | 31 ++++++++++---------- manimlib/typing.py | 2 ++ 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 313517ee..c1774f95 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -50,7 +50,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Callable, Iterable, Union, Tuple import numpy.typing as npt - from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array + from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict from moderngl.context import Context TimeBasedUpdater = Callable[["Mobject", float], "Mobject" | None] @@ -131,7 +131,7 @@ class Mobject(object): self.data = np.zeros(length, dtype=self.shader_dtype) def init_uniforms(self): - self.uniforms: dict[str, float | np.ndarray] = { + self.uniforms: UniformDict = { "is_fixed_in_frame": float(self.is_fixed_in_frame), "shading": np.array(self.shading, dtype=float), } @@ -1894,7 +1894,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.update(self.get_uniforms()) + self.shader_wrapper.update_program_uniforms(self.get_uniforms()) self.shader_wrapper.depth_test = self.depth_test return self.shader_wrapper @@ -1931,8 +1931,8 @@ class Mobject(object): shader_wrapper.generate_vao() self._data_has_changed = False for shader_wrapper in self.shader_wrappers: - shader_wrapper.uniforms.update(self.get_uniforms()) - shader_wrapper.uniforms.update(camera_uniforms) + shader_wrapper.update_program_uniforms(self.get_uniforms()) + shader_wrapper.update_program_uniforms(camera_uniforms, universal=True) shader_wrapper.pre_render() shader_wrapper.render() diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index e83c76e3..2260fc2e 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1264,11 +1264,6 @@ class VMobject(Mobject): self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), self.stroke_shader_wrapper.read_in(stroke_datas), ] - - for sw in shader_wrappers: - # Assume uniforms of the first family member - 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 aa733f8e..d4dc4889 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -7,7 +7,6 @@ import re import OpenGL.GL as gl import moderngl import numpy as np -from functools import lru_cache from manimlib.utils.iterables import resize_array from manimlib.utils.shaders import get_shader_code_from_file @@ -20,7 +19,8 @@ from manimlib.utils.shaders import release_texture from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import List, Optional + from typing import List, Optional, Dict + from manimlib.typing import UniformDict # Mobjects that should be rendered with @@ -37,7 +37,7 @@ class ShaderWrapper(object): vert_data: np.ndarray, 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 + uniforms: Optional[UniformDict] = 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, @@ -47,12 +47,13 @@ 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 = dict(uniforms or {}) + self.uniforms: UniformDict = dict() self.depth_test = depth_test self.render_primitive = render_primitive self.init_program_code() self.init_program() + self.update_program_uniforms(uniforms or dict()) if texture_paths is not None: self.init_textures(texture_paths) self.refresh_id() @@ -93,7 +94,7 @@ class ShaderWrapper(object): np.all(self.vert_indices == shader_wrapper.vert_indices), self.shader_folder == shader_wrapper.shader_folder, all( - np.all(self.uniforms[key] == shader_wrapper.uniforms[key]) + self.uniforms[key] == shader_wrapper.uniforms[key] for key in self.uniforms ), self.depth_test == shader_wrapper.depth_test, @@ -105,8 +106,6 @@ class ShaderWrapper(object): 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 @@ -217,20 +216,23 @@ class ShaderWrapper(object): 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): assert(self.vao is not None) self.vao.render() - def update_program_uniforms(self): + def update_program_uniforms(self, uniforms: UniformDict, universal: bool = False): if self.program is None: return - for name, value in 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 + for name, value in uniforms.items(): + if name not in self.program: + continue + if isinstance(value, np.ndarray) and value.ndim > 0: + value = tuple(value) + if universal and self.uniforms.get(name, None) == value: + continue + self.program[name].value = value + self.uniforms[name] = value def get_vertex_buffer_object(self, refresh: bool = True): if refresh: @@ -274,7 +276,6 @@ class FillShaderWrapper(ShaderWrapper): ): super().__init__(ctx, *args, **kwargs) - def render(self): vao = self.vao assert(vao is not None) diff --git a/manimlib/typing.py b/manimlib/typing.py index d4b68b3f..6ef9d22e 100644 --- a/manimlib/typing.py +++ b/manimlib/typing.py @@ -19,6 +19,8 @@ if TYPE_CHECKING: ] Selector = Union[SingleSelector, Iterable[SingleSelector]] + UniformDict = Dict[str, float | bool | np.ndarray | tuple] + # These are various alternate names for np.ndarray meant to specify # certain shapes. # From 3a175c1a4c61618a534a123ae23204349763ca9e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:02:28 -0800 Subject: [PATCH 02/36] Note that sorting surface indices affects data --- manimlib/mobject/types/surface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/types/surface.py b/manimlib/mobject/types/surface.py index 81855bac..faaa764d 100644 --- a/manimlib/mobject/types/surface.py +++ b/manimlib/mobject/types/surface.py @@ -195,6 +195,7 @@ class Surface(Mobject): ).reshape(shape) return points.reshape((nu * nv, *resolution[2:])) + @Mobject.affects_data def sort_faces_back_to_front(self, vect: Vect3 = OUT): tri_is = self.triangle_indices points = self.get_points() From 9ee9e1946af199d52705f0d071ca89428f84749c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:02:50 -0800 Subject: [PATCH 03/36] Use non-window fbo in scene.get_image --- manimlib/scene/scene.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 3d933970..23be48fa 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -288,13 +288,12 @@ class Scene(object): def get_image(self) -> Image: if self.window is not None: - self.window.size = self.camera.get_pixel_shape() - self.window.swap_buffers() - self.update_frame() - self.window.swap_buffers() + self.camera.use_window_fbo(False) + self.camera.clear() + self.camera.capture(*self.mobjects) image = self.camera.get_image() if self.window is not None: - self.window.to_default_position() + self.camera.use_window_fbo(True) return image def show(self) -> None: From 5803a005986e762ed45539decaca55c8c4f6cccd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:14:22 -0800 Subject: [PATCH 04/36] Use smaller fill_texture, adjusting winding-fill blending hack as is necessary --- .../shaders/quadratic_bezier_fill/frag.glsl | 4 ++-- .../shaders/quadratic_bezier_fill/geom.glsl | 2 +- manimlib/utils/shaders.py | 19 ++++++++++++++----- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index 352ad949..1db2bc88 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -30,9 +30,9 @@ void main() { 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. + blending with an (alpha = 1) color. */ - float a = 0.999 * frag_color.a; + float a = 0.98 * frag_color.a; if(winding && orientation < 0) a = -a / (1 - a); frag_color.a = a; diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 242f8621..9f1e8ad6 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -38,7 +38,7 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){ 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); + if(winding && color.rgb == vec3(0.0)) color.rgb += vec3(3.0 / 256); gl_Position = get_gl_Position(points[i]); EmitVertex(); } diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 1556015b..6fd3c03f 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -9,6 +9,7 @@ import numpy as np from manimlib.constants import DEFAULT_PIXEL_HEIGHT from manimlib.constants import DEFAULT_PIXEL_WIDTH +from manimlib.utils.customization import get_customization from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file @@ -107,10 +108,13 @@ 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) + cam_config = get_customization()['camera_resolutions'] + res_name = cam_config['default_resolution'] + size = tuple(map(int, cam_config[res_name].split("x"))) + # 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='f4') + 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) @@ -134,7 +138,9 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: uniform float h_nudge; in vec2 v_textcoord; - out vec4 frag_color; + out vec4 color; + + const float MIN_RGB = 2.0 / 256; void main() { // Apply poor man's anti-aliasing @@ -142,12 +148,15 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: 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 = + color = 0.25 * texture(Texture, tc0) + 0.25 * texture(Texture, tc1) + 0.25 * texture(Texture, tc2) + 0.25 * texture(Texture, tc3); - if(distance(frag_color.rgb, vec3(0.0)) < 1e-3) discard; + if(abs(color.r) < MIN_RGB && abs(color.g) < MIN_RGB && abs(color.b) < MIN_RGB) + discard; + // Counteract scaling in quadratic_bezier_frag + color = color / 0.98; //TODO, set gl_FragDepth; } ''', From e9333a908c0c239d318bd2df66fda47736a971eb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:38:38 -0800 Subject: [PATCH 05/36] Move camera.clear call into 'capture' --- manimlib/camera/camera.py | 1 + manimlib/scene/scene.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index f9906e9e..17ff2fc0 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -210,6 +210,7 @@ class Camera(object): # Rendering def capture(self, *mobjects: Mobject) -> None: + self.clear() self.refresh_uniforms() self.fbo.use() for mobject in mobjects: diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 23be48fa..5c29d3e6 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -289,7 +289,6 @@ class Scene(object): def get_image(self) -> Image: if self.window is not None: self.camera.use_window_fbo(False) - self.camera.clear() self.camera.capture(*self.mobjects) image = self.camera.get_image() if self.window is not None: @@ -311,7 +310,6 @@ class Scene(object): if self.window: self.window.clear() - self.camera.clear() self.camera.capture(*self.mobjects) if self.window: From bf2a609246a006699ce352c339b773a87a333fc3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:39:05 -0800 Subject: [PATCH 06/36] Have animation group collect parts as a VGroup if it can --- manimlib/animation/composition.py | 22 ++++++++++++------- .../animation/transform_matching_parts.py | 2 -- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 9e6beac4..a1c0ed37 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -6,6 +6,8 @@ from manimlib.animation.animation import Animation from manimlib.animation.animation import prepare_animation from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import Group +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import interpolate from manimlib.utils.iterables import remove_list_redundancies @@ -14,7 +16,7 @@ from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Callable + from typing import Callable, Optional from manimlib.mobject.mobject import Mobject from manimlib.scene.scene import Scene @@ -28,8 +30,8 @@ class AnimationGroup(Animation): *animations: Animation | _AnimationBuilder, run_time: float = -1, # If negative, default to sum of inputed animation runtimes lag_ratio: float = 0.0, - group: Mobject | None = None, - group_type: type = Group, + group: Optional[Mobject] = None, + group_type: Optional[type] = None, **kwargs ): self.animations = [prepare_animation(anim) for anim in animations] @@ -37,11 +39,15 @@ class AnimationGroup(Animation): self.max_end_time = max((awt[2] for awt in self.anims_with_timings), default=0) self.run_time = self.max_end_time if run_time < 0 else run_time self.lag_ratio = lag_ratio - self.group = group - if self.group is None: - self.group = group_type(*remove_list_redundancies( - [anim.mobject for anim in self.animations] - )) + mobs = remove_list_redundancies([a.mobject for a in self.animations]) + if group is not None: + self.group = group + if group_type is not None: + self.group = group_type(*mobs) + elif all(isinstance(anim.mobject, VMobject) for anim in animations): + self.group = VGroup(*mobs) + else: + self.group = Group(*mobs) super().__init__( self.group, diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 5fc8737b..67582366 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -32,7 +32,6 @@ class TransformMatchingParts(AnimationGroup): mismatch_animation: type = Transform, run_time: float = 2, lag_ratio: float = 0, - group_type: type = Group, **kwargs, ): self.source = source @@ -76,7 +75,6 @@ class TransformMatchingParts(AnimationGroup): *self.anims, run_time=run_time, lag_ratio=lag_ratio, - group_type=group_type, ) def add_transform( From 1bd6a77151cf1f12d801d58ec35036a6b1775f94 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 20:59:36 -0800 Subject: [PATCH 07/36] Don't use @stash_mobject_pointers on copy, be more explicit --- manimlib/mobject/mobject.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index c1774f95..1323b1da 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -591,19 +591,22 @@ class Mobject(object): result._shaders_initialized = False result._data_has_changed = True - @stash_mobject_pointers def copy(self, deep: bool = False): if deep: return self.deepcopy() result = copy.copy(self) - # The line above is only a shallow copy, so the internal - # data which are numpyu arrays or other mobjects still + result.parents = [] + result.target = None + result.save_state = None + + # copy.copy is only a shallow copy, so the internal + # data which are numpy arrays or other mobjects still # need to be further copied. result.data = self.data.copy() result.uniforms = { - key: np.array(value) + key: value.copy() if isinstance(value, np.ndarray) else value for key, value in self.uniforms.items() } From ab57b0acf09886656980281f40e0023d53c51f4f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 21:00:07 -0800 Subject: [PATCH 08/36] Ensure positive orientation on all SVG, not just PathStrings --- manimlib/mobject/svg/svg_mobject.py | 5 +---- manimlib/mobject/types/vectorized_mobject.py | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index d140dd19..cf921085 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -74,6 +74,7 @@ class SVGMobject(VMobject): super().__init__(**kwargs ) self.init_svg_mobject() + self.ensure_positive_orientation() # Rather than passing style into super().__init__ # do it after svg has been taken in @@ -320,10 +321,6 @@ 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, 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() else: diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 2260fc2e..01304556 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -827,6 +827,12 @@ class VMobject(Mobject): ) return normal + def ensure_positive_orientation(self, recurse=True): + for mob in self.get_family(recurse): + if mob.get_unit_normal()[2] < 0: + mob.reverse_points() + return self + # Alignment def align_points(self, vmobject: VMobject): winding = self._use_winding_fill and vmobject._use_winding_fill From 0f89349bb84cd6dce9760acd7801868c1f152537 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 21:00:20 -0800 Subject: [PATCH 09/36] Small clean up --- manimlib/shader_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index d4dc4889..94c6448e 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -290,9 +290,9 @@ class FillShaderWrapper(ShaderWrapper): texture_fbo.clear() texture_fbo.use() - vao.render() + vao.render(moderngl.TRIANGLE_STRIP) 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 From 7a59cc2f03c69babd9fd5146dc046ec2e10de551 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 22:40:29 -0800 Subject: [PATCH 10/36] Use gl.MAX to blend alphas in fill --- manimlib/shader_wrapper.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 94c6448e..aba3594a 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -290,9 +290,18 @@ class FillShaderWrapper(ShaderWrapper): texture_fbo.clear() texture_fbo.use() + self.ctx.blend_func = ( + # Ordinary blending for colors + moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA, + # Just take the max of the alphas, given the shenanigans + # with how alphas are being used to compute winding numbers + moderngl.ONE, moderngl.ONE + ) + self.ctx.blend_equation = moderngl.FUNC_ADD, moderngl.MAX vao.render(moderngl.TRIANGLE_STRIP) original_fbo.use() self.ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA) + self.ctx.blend_equation = moderngl.FUNC_ADD texture_vao.render(moderngl.TRIANGLE_STRIP) self.ctx.blend_func = moderngl.DEFAULT_BLENDING From 8f6c14ad5f7e3fe821789d23cac14aa1070d5447 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 22:41:23 -0800 Subject: [PATCH 11/36] Increase threshold for discarding fill fragments --- manimlib/utils/shaders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 6fd3c03f..2f326a8b 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -140,7 +140,7 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: in vec2 v_textcoord; out vec4 color; - const float MIN_RGB = 2.0 / 256; + const float MIN_RGB = 3.0 / 256; void main() { // Apply poor man's anti-aliasing From eab8edd51d70d38e8c486121b307f9ac6f3741e9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 22:41:36 -0800 Subject: [PATCH 12/36] Remove needless list(...) --- 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 1323b1da..fd7d1932 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -625,7 +625,7 @@ class Mobject(object): result._data_has_changed = True family = self.get_family() - for attr, value in list(self.__dict__.items()): + for attr, value in self.__dict__.items(): if isinstance(value, Mobject) and value is not self: if value in family: setattr(result, attr, result.family[self.family.index(value)]) From 63886478601ed97c0aa748d627d7d0a61a727da9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 22:51:14 -0800 Subject: [PATCH 13/36] Change to using glBlendFuncSeparate To please type checkers --- manimlib/shader_wrapper.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index aba3594a..0c45f8ab 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -275,6 +275,7 @@ class FillShaderWrapper(ShaderWrapper): **kwargs ): super().__init__(ctx, *args, **kwargs) + self.texture_fbo, self.texture_vao = get_fill_palette(self.ctx) def render(self): vao = self.vao @@ -286,22 +287,25 @@ class FillShaderWrapper(ShaderWrapper): return original_fbo = self.ctx.fbo - texture_fbo, texture_vao = get_fill_palette(self.ctx) - texture_fbo.clear() - texture_fbo.use() - self.ctx.blend_func = ( + self.texture_fbo.clear() + self.texture_fbo.use() + gl.glBlendFuncSeparate( # Ordinary blending for colors - moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA, + gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA, # Just take the max of the alphas, given the shenanigans # with how alphas are being used to compute winding numbers - moderngl.ONE, moderngl.ONE + gl.GL_ONE, gl.GL_ONE, ) + gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX) self.ctx.blend_equation = moderngl.FUNC_ADD, moderngl.MAX + vao.render(moderngl.TRIANGLE_STRIP) original_fbo.use() - self.ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA) - self.ctx.blend_equation = moderngl.FUNC_ADD - texture_vao.render(moderngl.TRIANGLE_STRIP) - self.ctx.blend_func = moderngl.DEFAULT_BLENDING + gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA) + gl.glBlendEquation(gl.GL_FUNC_ADD) + + self.texture_vao.render(moderngl.TRIANGLE_STRIP) + + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) From 422c9cebd22e2b004a15807d57b46522ed705e4e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:05:01 -0800 Subject: [PATCH 14/36] Only trigger triangulation for non-winding fill --- manimlib/animation/creation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index cec23ab0..4ad116bd 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -100,7 +100,8 @@ class DrawBorderThenFill(Animation): def begin(self) -> None: # Trigger triangulation calculation for submob in self.mobject.get_family(): - submob.get_triangulation() + if not submob._use_winding_fill: + submob.get_triangulation() self.outline = self.get_outline() super().begin() From 28636727407a115c01fb5acdd795dc1d787e6665 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:05:09 -0800 Subject: [PATCH 15/36] Small clean up --- manimlib/mobject/mobject.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index fd7d1932..9ee4154b 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1768,20 +1768,26 @@ class Mobject(object): self.locked_data_keys = set(keys) def lock_matching_data(self, mobject1: Mobject, mobject2: Mobject): - for sm, sm1, sm2 in zip(self.get_family(), mobject1.get_family(), mobject2.get_family()): - if sm.data.dtype == sm1.data.dtype == sm2.data.dtype: - names = sm.data.dtype.names - sm.lock_data(filter( - lambda name: arrays_match(sm1.data[name], sm2.data[name]), - names, - )) - sm.const_data_keys = set(filter( - lambda name: all( - array_is_constant(mob.data[name]) - for mob in (sm, sm1, sm2) - ), - names - )) + tuples = zip( + self.get_family(), + mobject1.get_family(), + mobject2.get_family(), + ) + for sm, sm1, sm2 in tuples: + if not sm.data.dtype == sm1.data.dtype == sm2.data.dtype: + continue + names = sm.data.dtype.names + sm.lock_data(filter( + lambda name: arrays_match(sm1.data[name], sm2.data[name]), + names, + )) + sm.const_data_keys = set(filter( + lambda name: all( + array_is_constant(mob.data[name]) + for mob in (sm, sm1, sm2) + ), + names + )) return self From 79039bde61f3235377ad310b139b9eec122da210 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:42:03 -0800 Subject: [PATCH 16/36] Fix Arrow --- manimlib/mobject/geometry.py | 16 +++++++--------- .../shaders/quadratic_bezier_stroke/geom.glsl | 6 ++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 7e73b8a0..31571991 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -715,22 +715,20 @@ class Arrow(Line): else: alpha = tip_len / arc_len self.pointwise_become_partial(self, 0, 1 - alpha) - self.start_new_path(self.get_points()[-1]) + # Dumb that this is needed + self.start_new_path(self.point_from_proportion(0.99)) self.add_line_to(prev_end) return self + @Mobject.affects_data def create_tip_with_stroke_width(self): - if not self.has_points(): + if self.get_num_points() < 3: return self - width = min( - self.max_stroke_width, + tip_width = self.tip_width_ratio * min( + float(self.get_stroke_width()), self.max_width_to_length_ratio * self.get_length(), ) - widths_array = np.full(self.get_num_points(), width) - if len(widths_array) > 3: - tip_width = self.tip_width_ratio * width - widths_array[-3:] = tip_width * np.linspace(1, 0, 3) - self.set_stroke(width=widths_array) + self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3) return self def reset_tip(self): diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 96e3f1b2..6e822f09 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -167,7 +167,9 @@ void main() { vec3 v01 = normalize(p1 - p0); vec3 v12 = normalize(p2 - p1); - float cos_angle = normalize(v_joint_product[1]).w; + vec4 jp1 = v_joint_product[1]; + float norm = length(jp1); + float cos_angle = (norm > 0) ? (jp1 / norm).w : 1.0; is_linear = float(cos_angle > COS_THRESHOLD); // We want to change the coordinates to a space where the curve @@ -195,7 +197,7 @@ void main() { float sign = vec2(-1, 1)[i % 2]; // In this case, we only really care about // the v coordinate - uv_coords = vec2(0, sign * (0.5 * max_sw + scaled_aaw)); + uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw)); uv_anti_alias_width = scaled_aaw; uv_stroke_width = stroke_width; }else{ From b21e470e693851519694e350b284c43b471ff19b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:43:21 -0800 Subject: [PATCH 17/36] In append_vectorized_mobject, append data as well as points --- 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 01304556..a26cac2a 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -669,6 +669,8 @@ class VMobject(Mobject): def append_vectorized_mobject(self, vmobject: VMobject): self.add_subpath(vmobject.get_points()) + n = vmobject.get_num_points() + self.data[-n:] = vmobject.data return self # From de7545e5fac13bb60dbb46447032d6cf5371c688 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:43:34 -0800 Subject: [PATCH 18/36] Tiny tweak to array_is_constant --- manimlib/utils/iterables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/iterables.py b/manimlib/utils/iterables.py index b57225e8..3b7dbfba 100644 --- a/manimlib/utils/iterables.py +++ b/manimlib/utils/iterables.py @@ -133,7 +133,7 @@ def arrays_match(arr1: np.ndarray, arr2: np.ndarray) -> bool: def array_is_constant(arr: np.ndarray) -> bool: - return len(arr) > 0 and not (arr - arr[0]).any() + return len(arr) > 0 and (arr == arr[0]).all() def hash_obj(obj: object) -> int: From 86fb1d82f5bd7b626a810b35f895c91c79f5e570 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:44:33 -0800 Subject: [PATCH 19/36] Typo fix --- 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 9ee4154b..5379575b 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -599,7 +599,7 @@ class Mobject(object): result.parents = [] result.target = None - result.save_state = None + result.saved_state = None # copy.copy is only a shallow copy, so the internal # data which are numpy arrays or other mobjects still From 746b52cda5d1167413de5084f7b9e8a2ba01b144 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 26 Jan 2023 23:51:05 -0800 Subject: [PATCH 20/36] Okay, actually fix Arrow --- manimlib/mobject/geometry.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 31571991..a8b665d7 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -685,7 +685,6 @@ class Arrow(Line): self.width_to_tip_len = width_to_tip_len self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio self.max_width_to_length_ratio = max_width_to_length_ratio - self.max_stroke_width = stroke_width super().__init__( start, end, stroke_color=stroke_color, @@ -716,7 +715,7 @@ class Arrow(Line): alpha = tip_len / arc_len self.pointwise_become_partial(self, 0, 1 - alpha) # Dumb that this is needed - self.start_new_path(self.point_from_proportion(0.99)) + self.start_new_path(self.point_from_proportion(1 - 1e-5)) self.add_line_to(prev_end) return self @@ -728,6 +727,7 @@ class Arrow(Line): float(self.get_stroke_width()), self.max_width_to_length_ratio * self.get_length(), ) + self.data['stroke_width'][:-3] = self.data['stroke_width'][0] self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3) return self @@ -745,13 +745,14 @@ class Arrow(Line): *args, **kwargs ): super().set_stroke(color=color, width=width, *args, **kwargs) - if isinstance(width, numbers.Number): - self.max_stroke_width = width - self.create_tip_with_stroke_width() + if self.has_points(): + self.reset_tip() return self def _handle_scale_side_effects(self, scale_factor: float): - return self.reset_tip() + if scale_factor != 1.0: + self.reset_tip() + return self class FillArrow(Line): From 1707958e0fd98cbccb5eba194eec2447ec31b6d8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 08:26:54 -0800 Subject: [PATCH 21/36] Clean up fill shader a bit --- manimlib/shader_wrapper.py | 11 ++-- .../shaders/quadratic_bezier_fill/frag.glsl | 2 +- .../shaders/quadratic_bezier_fill/geom.glsl | 2 - manimlib/utils/shaders.py | 52 ++++++++++++------- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 0c45f8ab..0b278752 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -13,7 +13,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_fill_palette +from manimlib.utils.shaders import get_fill_canvas from manimlib.utils.shaders import release_texture from typing import TYPE_CHECKING @@ -275,7 +275,7 @@ class FillShaderWrapper(ShaderWrapper): **kwargs ): super().__init__(ctx, *args, **kwargs) - self.texture_fbo, self.texture_vao = get_fill_palette(self.ctx) + self.fill_canvas = get_fill_canvas(self.ctx) def render(self): vao = self.vao @@ -287,9 +287,10 @@ class FillShaderWrapper(ShaderWrapper): return original_fbo = self.ctx.fbo + texture_fbo, texture_vao, null_rgb = self.fill_canvas - self.texture_fbo.clear() - self.texture_fbo.use() + texture_fbo.clear(*null_rgb, 0.0) + texture_fbo.use() gl.glBlendFuncSeparate( # Ordinary blending for colors gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA, @@ -306,6 +307,6 @@ class FillShaderWrapper(ShaderWrapper): gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glBlendEquation(gl.GL_FUNC_ADD) - self.texture_vao.render(moderngl.TRIANGLE_STRIP) + texture_vao.render(moderngl.TRIANGLE_STRIP) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index 1db2bc88..a7467b49 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -32,7 +32,7 @@ void main() { cap is to make sure the original fragment color can be recovered even after blending with an (alpha = 1) color. */ - float a = 0.98 * frag_color.a; + float a = 0.99 * frag_color.a; if(winding && orientation < 0) a = -a / (1 - a); frag_color.a = a; diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 9f1e8ad6..f8083c42 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -37,8 +37,6 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){ uv_coords = SIMPLE_QUADRATIC[i]; 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(3.0 / 256); gl_Position = get_gl_Position(points[i]); EmitVertex(); } diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 2f326a8b..203b6f66 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -103,10 +103,16 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str: @lru_cache() -def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: +def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]: """ - Creates a texture, loaded into a frame buffer, and a vao - which can display that texture as a simple quad onto a screen. + Because VMobjects with fill are rendered in a funny way, using + alpha blending to effectively compute the winding number around + each pixel, they need to be rendered to a separate texture, which + is then composited onto the ordinary frame buffer. + + This returns a texture, loaded into a frame buffer, and a vao + which can display that texture as a simple quad onto a screen, + along with the rgb value which is meant to be discarded. """ cam_config = get_customization()['camera_resolutions'] res_name = cam_config['default_resolution'] @@ -118,6 +124,12 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used texture_fbo = ctx.framebuffer(texture, depth_buffer) + # We'll paint onto a canvas with initially negative rgbs, and + # discard any pixels remaining close to this value. This is + # because alphas are effectively being used for another purpose, + # and + null_rgb = (-0.25, -0.25, -0.25) + simple_program = ctx.program( vertex_shader=''' #version 330 @@ -136,27 +148,30 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: uniform sampler2D Texture; uniform float v_nudge; uniform float h_nudge; + uniform vec3 null_rgb; in vec2 v_textcoord; out vec4 color; - const float MIN_RGB = 3.0 / 256; + const float MIN_DIST_TO_NULL = 0.2; void main() { // Apply poor man's anti-aliasing - 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); - color = - 0.25 * texture(Texture, tc0) + - 0.25 * texture(Texture, tc1) + - 0.25 * texture(Texture, tc2) + - 0.25 * texture(Texture, tc3); - if(abs(color.r) < MIN_RGB && abs(color.g) < MIN_RGB && abs(color.b) < MIN_RGB) - discard; - // Counteract scaling in quadratic_bezier_frag - color = color / 0.98; + vec2 nudges[4] = vec2[4]( + vec2(0, 0), + vec2(0, h_nudge), + vec2(v_nudge, 0), + vec2(v_nudge, h_nudge) + ); + color = vec4(0.0); + for(int i = 0; i < 4; i++){ + color += 0.25 * texture(Texture, v_textcoord + nudges[i]); + } + if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard; + + // Un-blend from the null value + color.rgb -= (1 - color.a) * null_rgb; + //TODO, set gl_FragDepth; } ''', @@ -166,6 +181,7 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: # Half pixel width/height simple_program['h_nudge'].value = 0.5 / size[0] simple_program['v_nudge'].value = 0.5 / size[1] + simple_program['null_rgb'].value = null_rgb verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) fill_texture_vao = ctx.simple_vertex_array( @@ -173,4 +189,4 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]: ctx.buffer(verts.astype('f4').tobytes()), 'texcoord', ) - return (texture_fbo, fill_texture_vao) + return (texture_fbo, fill_texture_vao, null_rgb) From 97e4c254536623946346fb76fddd874075fd16a7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 08:29:41 -0800 Subject: [PATCH 22/36] Add comment --- manimlib/utils/shaders.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 203b6f66..643099a3 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -127,7 +127,11 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, # We'll paint onto a canvas with initially negative rgbs, and # discard any pixels remaining close to this value. This is # because alphas are effectively being used for another purpose, - # and + # and we don't want to overlap with any colors one might actually + # use. It should be negative enough to be distinguishable from + # ordinary colors with some margin, but the farther it's pulled back + # from zero the more it will be true that overlapping filled objects + # with transparency have an unnaturally bright composition. null_rgb = (-0.25, -0.25, -0.25) simple_program = ctx.program( From 40ae4819798e90bd0249feed4a5ba50ff1a3b357 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 10:01:37 -0800 Subject: [PATCH 23/36] Marginally better vbo/ibo tracking --- manimlib/shader_wrapper.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 0b278752..38f1f606 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -56,12 +56,9 @@ class ShaderWrapper(object): self.update_program_uniforms(uniforms or dict()) if texture_paths is not None: self.init_textures(texture_paths) + self.init_vao() 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( @@ -88,6 +85,11 @@ class ShaderWrapper(object): tid = get_texture_id(texture) self.uniforms[name] = tid + def init_vao(self): + self.vbo = None + self.ibo = None + self.vao = None + def __eq__(self, shader_wrapper: ShaderWrapper): return all(( np.all(self.vert_data == shader_wrapper.vert_data), @@ -106,9 +108,7 @@ class ShaderWrapper(object): result.ctx = self.ctx result.vert_data = self.vert_data.copy() result.vert_indices = self.vert_indices.copy() - result.vao = None - result.vbo = None - result.ibo = None + result.init_vao() return result def is_valid(self) -> bool: @@ -247,8 +247,9 @@ class ShaderWrapper(object): 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) + vbo = self.vbo = self.get_vertex_buffer_object(refresh) + ibo = self.ibo = self.get_index_buffer_object(refresh) + # Vertex array object self.vao = self.ctx.vertex_array( program=self.program, From 3a0916fe3af397015257ec698c4ac96ecc7cc4ef Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 10:12:53 -0800 Subject: [PATCH 24/36] Reorganize fbo initialization --- manimlib/camera/camera.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 17ff2fc0..8d6c0ec3 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -17,6 +17,7 @@ from manimlib.utils.color import color_to_rgba from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Optional from manimlib.typing import ManimColor, Vect3 from manimlib.window import Window @@ -24,8 +25,8 @@ if TYPE_CHECKING: class Camera(object): def __init__( self, - window: Window | None = None, - background_image: str | None = None, + window: Optional[Window] = None, + background_image: Optional[str] = None, frame_config: dict = dict(), pixel_width: int = DEFAULT_PIXEL_WIDTH, pixel_height: int = DEFAULT_PIXEL_HEIGHT, @@ -62,31 +63,38 @@ class Camera(object): )) self.uniforms = dict() self.init_frame(**frame_config) - self.init_context(window) + self.init_context() + self.init_fbo() self.init_light_source() def init_frame(self, **config) -> None: self.frame = CameraFrame(**config) - def init_context(self, window: Window | None = None) -> None: - self.window = window - if window is None: + def init_context(self) -> None: + if self.window is None: self.ctx = moderngl.create_standalone_context() - self.fbo = self.get_fbo(self.samples) else: - self.ctx = window.ctx - 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 = self.window.ctx self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) self.ctx.enable(moderngl.BLEND) + def init_fbo(self) -> None: + # This is the buffer used when writing to a video/image file + self.fbo_for_files = self.get_fbo(self.samples) + # This is the frame buffer we'll draw into when emitting frames self.draw_fbo = self.get_fbo(samples=0) + if self.window is None: + self.window_fbo = None + self.fbo = self.fbo_for_files + else: + self.window_fbo = self.ctx.detect_framebuffer() + self.fbo = self.window_fbo + + self.fbo.use() + def init_light_source(self) -> None: self.light_source = Point(self.light_source_position) From 1c432dd6dc347d4c58fd1d49b6f5cd50d24517f9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 10:48:06 -0800 Subject: [PATCH 25/36] Small refactor to stroke geom shader --- .../shaders/quadratic_bezier_stroke/geom.glsl | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 6e822f09..201ab4ab 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -49,6 +49,12 @@ vec3 get_joint_unit_normal(vec4 joint_product){ } +vec4 normalized_joint_product(vec4 joint_product){ + float norm = length(joint_product); + return (norm > 1e-10) ? joint_product / norm : vec4(0.0, 0.0, 0.0, 1.0); +} + + void create_joint( vec4 joint_product, vec3 unit_tan, @@ -78,6 +84,25 @@ void create_joint( changing_c1 = static_c1 + shift * unit_tan; } +vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw){ + /* + Perpendicular vectors to the left of the curve + */ + float buff = 0.5 * v_stroke_width[index] + aaw; + // Add correction for sharp angles to prevent weird bevel effects + if(joint_product.w < -0.9) buff *= 10 * (joint_product.w + 1.0); + vec3 normal = get_joint_unit_normal(joint_product); + // Set global unit normal + unit_normal = normal; + // Choose the "outward" normal direction + if(normal.z < 0) normal *= -1; + if(bool(flat_stroke)){ + return buff * normalize(cross(normal, tangent)); + }else{ + return buff * normalize(cross(camera_position - point, tangent)); + } +} + // This function is responsible for finding the corners of // a bounding region around the bezier curve, which can be // emitted as a triangle fan, with vertices vaguely close @@ -95,40 +120,15 @@ void get_corners( float aaw, out vec3 corners[6] ){ - - float buff0 = 0.5 * v_stroke_width[0] + aaw; - float buff2 = 0.5 * v_stroke_width[2] + aaw; - - vec4 jp0 = normalize(v_joint_product[0]); - vec4 jp2 = normalize(v_joint_product[2]); - - // Add correction for sharp angles to prevent weird bevel effects - if(jp0.w < -0.9) buff0 *= 10 * (jp0.w + 1.0); - if(jp2.w < -0.9) buff2 *= 10 * (jp2.w + 1.0); - - // Unit normal and joint angles - vec3 normal0 = get_joint_unit_normal(jp0); - vec3 normal2 = get_joint_unit_normal(jp2); - // Set global unit normal - unit_normal = normal0; - - // Choose the "outward" normal direction - normal0 *= sign(normal0.z); - normal2 *= sign(normal2.z); - - vec3 p0_perp; - vec3 p2_perp; - if(bool(flat_stroke)){ - // Perpendicular vectors to the left of the curve - p0_perp = buff0 * normalize(cross(normal0, v01)); - p2_perp = buff2 * normalize(cross(normal2, v12)); - }else{ - // p0_perp = buff0 * normal0; - // p2_perp = buff2 * normal2; - p0_perp = buff0 * normalize(cross(camera_position - p0, v01)); - p2_perp = buff2 * normalize(cross(camera_position - p2, v12)); - } + bool linear = bool(is_linear); + vec4 jp0 = normalized_joint_product(v_joint_product[0]); + vec4 jp2 = normalized_joint_product(v_joint_product[2]); + vec3 p0_perp = get_perp(0, jp0, p0, v01, aaw); + vec3 p2_perp = get_perp(2, jp2, p2, v12, aaw); vec3 p1_perp = 0.5 * (p0_perp + p2_perp); + if(linear){ + p1_perp *= (0.5 * v_stroke_width[1] + aaw) / length(p1_perp); + } // The order of corners should be for a triangle_strip. vec3 c0 = p0 + p0_perp; @@ -139,14 +139,15 @@ void get_corners( vec3 c5 = p2 - p2_perp; // Move the inner middle control point to make // room for the curve - float orientation = dot(normal0, v_joint_product[1].xyz); - if(orientation >= 0.0) c2 = 0.5 * (c0 + c4); - else if(orientation < 0.0) c3 = 0.5 * (c1 + c5); + // float orientation = dot(unit_normal, v_joint_product[1].xyz); + float orientation = v_joint_product[1].z; + if(!linear && orientation >= 0.0) c2 = 0.5 * (c0 + c4); + else if(!linear && orientation < 0.0) c3 = 0.5 * (c1 + c5); // Account for previous and next control points if(bool(flat_stroke)){ - create_joint(jp0, v01, buff0, c1, c1, c0, c0); - create_joint(jp2, -v12, buff2, c5, c5, c4, c4); + create_joint(jp0, v01, length(p0_perp), c1, c1, c0, c0); + create_joint(jp2, -v12, length(p2_perp), c5, c5, c4, c4); } corners = vec3[6](c0, c1, c2, c3, c4, c5); @@ -167,10 +168,9 @@ void main() { vec3 v01 = normalize(p1 - p0); vec3 v12 = normalize(p2 - p1); - vec4 jp1 = v_joint_product[1]; - float norm = length(jp1); - float cos_angle = (norm > 0) ? (jp1 / norm).w : 1.0; - is_linear = float(cos_angle > COS_THRESHOLD); + + vec4 jp1 = normalized_joint_product(v_joint_product[1]); + is_linear = float(jp1.w > COS_THRESHOLD); // We want to change the coordinates to a space where the curve // coincides with y = x^2, between some values x0 and x2. Or, in From 86fb69c5bb4ae92f648a809623fd1fc7d3902a00 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 12:35:43 -0800 Subject: [PATCH 26/36] Track unit normal for fill --- manimlib/mobject/types/vectorized_mobject.py | 34 ++++++++++++++----- .../shaders/quadratic_bezier_fill/frag.glsl | 3 +- .../shaders/quadratic_bezier_fill/geom.glsl | 14 ++++++-- .../shaders/quadratic_bezier_fill/vert.glsl | 3 ++ 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a26cac2a..5aea17c9 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -62,8 +62,9 @@ class VMobject(Mobject): ('joint_product', np.float32, (4,)), ('fill_rgba', np.float32, (4,)), ('base_point', np.float32, (3,)), + ('unit_normal', np.float32, (3,)), ]) - fill_data_names = ['point', 'fill_rgba', 'base_point'] + fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal'] stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product'] fill_render_primitive: int = moderngl.TRIANGLE_STRIP @@ -827,8 +828,25 @@ class VMobject(Mobject): points[1] - points[0], points[2] - points[1], ) + self.data["unit_normal"][:] = normal return normal + def refresh_unit_normal(self): + self.get_unit_normal() + return self + + def rotate( + self, + angle: float, + axis: Vect3 = OUT, + about_point: Vect3 | None = None, + **kwargs + ): + super().rotate(angle, axis, about_point, **kwargs) + for mob in self.get_family(): + mob.refresh_unit_normal() + return self + def ensure_positive_orientation(self, recurse=True): for mob in self.get_family(recurse): if mob.get_unit_normal()[2] < 0: @@ -1148,6 +1166,7 @@ class VMobject(Mobject): self.refresh_triangulation() if refresh_joints: self.get_joint_products(refresh=True) + self.get_unit_normal() return self @triggers_refreshed_triangulation @@ -1157,16 +1176,14 @@ class VMobject(Mobject): return self @triggers_refreshed_triangulation - def reverse_points(self): + def reverse_points(self, recurse: bool = True): # This will reset which anchors are # considered path ends - for mob in self.get_family(): + for mob in self.get_family(recurse): if not mob.has_points(): continue inner_ends = mob.get_subpath_end_indices()[:-1] mob.data["point"][inner_ends + 1] = mob.data["point"][inner_ends + 2] - super().reverse_points() - return self @triggers_refreshed_triangulation def set_data(self, data: np.ndarray): @@ -1248,11 +1265,12 @@ class VMobject(Mobject): back_stroke_data = [] for submob in family: if submob.has_fill(): - submob.data["base_point"][:] = submob.data["point"][0] - fill_datas.append(submob.data[fill_names]) + data = submob.data[fill_names] + data["base_point"][:] = data["point"][0] + fill_datas.append(data) if self._use_winding_fill: # Add dummy - fill_datas.append(submob.data[fill_names][-1:]) + fill_datas.append(data[-1:]) else: fill_indices.append(submob.get_triangulation()) if submob.has_stroke(): diff --git a/manimlib/shaders/quadratic_bezier_fill/frag.glsl b/manimlib/shaders/quadratic_bezier_fill/frag.glsl index a7467b49..199efbec 100644 --- a/manimlib/shaders/quadratic_bezier_fill/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/frag.glsl @@ -7,6 +7,7 @@ in float fill_all; in float orientation; in vec2 uv_coords; in vec3 point; +in vec3 unit_normal; out vec4 frag_color; @@ -14,7 +15,7 @@ out vec4 frag_color; void main() { if (color.a == 0) discard; - frag_color = finalize_color(color, point, vec3(0.0, 0.0, 1.0)); + frag_color = finalize_color(color, point, unit_normal); /* 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 f8083c42..4a0d6c70 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -9,11 +9,13 @@ in vec3 verts[3]; in vec4 v_color[3]; in vec3 v_base_point[3]; in float v_vert_index[3]; +in vec3 v_unit_normal[3]; out vec4 color; out float fill_all; out float orientation; out vec3 point; +out vec3 unit_normal; // uv space is where the curve coincides with y = x^2 out vec2 uv_coords; @@ -26,12 +28,18 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3]( // Analog of import for manim only #INSERT get_gl_Position.glsl -#INSERT get_unit_normal.glsl void emit_triangle(vec3 points[3], vec4 v_color[3]){ - vec3 unit_normal = get_unit_normal(points[0], points[1], points[2]); - orientation = winding ? sign(unit_normal.z) : 1.0; + unit_normal = v_unit_normal[1]; + orientation = 1.0; + if(winding){ + orientation = sign(determinant(mat3( + v_unit_normal[1], + points[1] - points[0], + points[2] - points[0] + ))); + } for(int i = 0; i < 3; i++){ uv_coords = SIMPLE_QUADRATIC[i]; diff --git a/manimlib/shaders/quadratic_bezier_fill/vert.glsl b/manimlib/shaders/quadratic_bezier_fill/vert.glsl index 5818216a..a15752c4 100644 --- a/manimlib/shaders/quadratic_bezier_fill/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/vert.glsl @@ -3,15 +3,18 @@ in vec3 point; in vec4 fill_rgba; in vec3 base_point; +in vec3 unit_normal; out vec3 verts; // Bezier control point out vec4 v_color; out vec3 v_base_point; +out vec3 v_unit_normal; out float v_vert_index; void main(){ verts = point; v_color = fill_rgba; v_base_point = base_point; + v_unit_normal = unit_normal; v_vert_index = gl_VertexID; } \ No newline at end of file From e57ca4e1ee4995714e38a70d21a07914e343a732 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 12:43:21 -0800 Subject: [PATCH 27/36] Track orientation for non-winding fill --- manimlib/mobject/types/vectorized_mobject.py | 2 -- .../shaders/quadratic_bezier_fill/geom.glsl | 17 +++++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 5aea17c9..da210a23 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1082,8 +1082,6 @@ class VMobject(Mobject): inner_tri_indices = iti[~(null1 | null2).repeat(3)] 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 diff --git a/manimlib/shaders/quadratic_bezier_fill/geom.glsl b/manimlib/shaders/quadratic_bezier_fill/geom.glsl index 4a0d6c70..462f00aa 100644 --- a/manimlib/shaders/quadratic_bezier_fill/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_fill/geom.glsl @@ -31,15 +31,11 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3]( void emit_triangle(vec3 points[3], vec4 v_color[3]){ - unit_normal = v_unit_normal[1]; - orientation = 1.0; - if(winding){ - orientation = sign(determinant(mat3( - v_unit_normal[1], - points[1] - points[0], - points[2] - points[0] - ))); - } + orientation = sign(determinant(mat3( + unit_normal, + points[1] - points[0], + points[2] - points[0] + ))); for(int i = 0; i < 3; i++){ uv_coords = SIMPLE_QUADRATIC[i]; @@ -69,7 +65,8 @@ void main(){ // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - vec3 mid_vert; + unit_normal = v_unit_normal[1]; + if(winding){ // Emit main triangle fill_all = 1.0; From 35c19fe8a7e0b98a5194e56fe5b598e90e5d91d3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 14:48:31 -0800 Subject: [PATCH 28/36] Edit is_fixed_in_frame --- manimlib/animation/indication.py | 2 +- manimlib/mobject/mobject.py | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index 8275a26a..cd01c359 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -265,7 +265,7 @@ class FlashAround(VShowPassingFlash): **kwargs ): path = self.get_path(mobject, buff) - if mobject.is_fixed_in_frame: + if mobject.is_fixed_in_frame(): path.fix_in_frame() path.insert_n_curves(n_inserted_curves) path.set_points(path.get_points_without_null_curves()) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 5379575b..540e8250 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -88,7 +88,7 @@ class Mobject(object): self.opacity = opacity self.shading = shading self.texture_paths = texture_paths - self.is_fixed_in_frame = is_fixed_in_frame + self._is_fixed_in_frame = is_fixed_in_frame self.depth_test = depth_test # Internal state @@ -132,7 +132,7 @@ class Mobject(object): def init_uniforms(self): self.uniforms: UniformDict = { - "is_fixed_in_frame": float(self.is_fixed_in_frame), + "is_fixed_in_frame": float(self._is_fixed_in_frame), "shading": np.array(self.shading, dtype=float), } @@ -1808,17 +1808,19 @@ class Mobject(object): return wrapper @affects_shader_info_id - def fix_in_frame(self): - self.uniforms["is_fixed_in_frame"] = 1.0 - self.is_fixed_in_frame = True + def fix_in_frame(self, recurse: bool = True): + for mob in self.get_family(recurse): + mob.uniforms["is_fixed_in_frame"] = 1.0 return self @affects_shader_info_id def unfix_from_frame(self): self.uniforms["is_fixed_in_frame"] = 0.0 - self.is_fixed_in_frame = False return self + def is_fixed_in_frame(self) -> bool: + return bool(self.uniforms["is_fixed_in_frame"]) + @affects_shader_info_id def apply_depth_test(self): self.depth_test = True @@ -2057,7 +2059,7 @@ 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): + if any(m.is_fixed_in_frame() for m in mobjects): self.fix_in_frame() def __add__(self, other: Mobject | Group): From 1f6363821b82f5793e57c1b667c70f1d67c2f697 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 14:48:57 -0800 Subject: [PATCH 29/36] Have VMobject inherit children uniforms when rendering --- 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 da210a23..a2d56f01 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1288,6 +1288,8 @@ class VMobject(Mobject): self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), self.stroke_shader_wrapper.read_in(stroke_datas), ] + # TODO, account for submob uniforms separately? + self.uniforms.update(family[0].uniforms) return [sw for sw in shader_wrappers if len(sw.vert_data) > 0] From d5b1a1725da76eb305bb81a14ea1144cbb4942e4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 15:15:16 -0800 Subject: [PATCH 30/36] Allow Mobject.remove to remove any family member, not just immediate submobjects --- manimlib/mobject/mobject.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 540e8250..18efb62a 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -408,14 +408,15 @@ class Mobject(object): self.assemble_family() return self - def remove(self, *mobjects: Mobject, reassemble: bool = True): - for mobject in mobjects: - if mobject in self.submobjects: - self.submobjects.remove(mobject) - if self in mobject.parents: - mobject.parents.remove(self) - if reassemble: - self.assemble_family() + def remove(self, *to_remove: Mobject, reassemble: bool = True): + for parent in self.get_family(): + for child in to_remove: + if child in parent.submobjects: + parent.submobjects.remove(child) + if parent in child.parents: + child.parents.remove(parent) + if reassemble: + parent.assemble_family() return self def add_to_back(self, *mobjects: Mobject): From 3a01eb31bd1a9fd737e230f5c7889a92da45e571 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 16:02:47 -0800 Subject: [PATCH 31/36] Remove group_type arg --- manimlib/animation/transform_matching_parts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 67582366..6f44c16d 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -149,7 +149,6 @@ class TransformMatchingStrings(TransformMatchingParts): super().__init__( source, target, matched_pairs=matched_pairs, - group_type=VGroup, **kwargs, ) From 71ef39ea5be5f9c7b132310f2a5f71647f0e58b3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 16:15:20 -0800 Subject: [PATCH 32/36] Remove "poor man's anti-aliasing" for Fill and instead render a small border width for fill --- manimlib/mobject/types/vectorized_mobject.py | 36 +++++++++++++++----- manimlib/utils/shaders.py | 17 +-------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a2d56f01..1e883574 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -63,6 +63,7 @@ class VMobject(Mobject): ('fill_rgba', np.float32, (4,)), ('base_point', np.float32, (3,)), ('unit_normal', np.float32, (3,)), + ('fill_border_width', np.float32, (1,)), ]) fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal'] stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product'] @@ -92,6 +93,7 @@ class VMobject(Mobject): use_simple_quadratic_approx: bool = False, # Measured in pixel widths anti_alias_width: float = 1.0, + fill_border_width: float = 0.5, use_winding_fill: bool = True, **kwargs ): @@ -107,6 +109,7 @@ class VMobject(Mobject): self.flat_stroke = flat_stroke self.use_simple_quadratic_approx = use_simple_quadratic_approx self.anti_alias_width = anti_alias_width + self.fill_border_width = fill_border_width self._use_winding_fill = use_winding_fill self.needs_new_triangulation = True @@ -164,6 +167,7 @@ class VMobject(Mobject): self.set_fill( color=self.fill_color, opacity=self.fill_opacity, + border_width=self.fill_border_width, ) self.set_stroke( color=self.stroke_color, @@ -195,9 +199,13 @@ class VMobject(Mobject): self, color: ManimColor | Iterable[ManimColor] = None, opacity: float | Iterable[float] | None = None, + border_width: float | None = None, recurse: bool = True ): self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse) + if border_width is not None: + for mob in self.get_family(recurse): + mob.data["fill_border_width"] = border_width return self def set_stroke( @@ -1258,11 +1266,15 @@ class VMobject(Mobject): # Build up data lists fill_datas = [] + fill_border_datas = [] fill_indices = [] stroke_datas = [] - back_stroke_data = [] + back_stroke_datas = [] for submob in family: - if submob.has_fill(): + submob.get_joint_products() + has_fill = submob.has_fill() + has_stroke = submob.has_stroke() + if has_fill: data = submob.data[fill_names] data["base_point"][:] = data["point"][0] fill_datas.append(data) @@ -1271,12 +1283,16 @@ class VMobject(Mobject): fill_datas.append(data[-1:]) else: fill_indices.append(submob.get_triangulation()) - if submob.has_stroke(): - submob.get_joint_products() - if submob.stroke_behind: - lst = back_stroke_data - else: - lst = stroke_datas + # Add fill border + if not has_stroke: + names = list(stroke_names) + names[names.index('stroke_rgba')] = 'fill_rgba' + names[names.index('stroke_width')] = 'fill_border_width' + border_stroke_data = submob.data[names] + fill_border_datas.append(border_stroke_data) + fill_border_datas.append(border_stroke_data[-1:]) + if has_stroke: + lst = back_stroke_datas if submob.stroke_behind else stroke_datas lst.append(submob.data[stroke_names]) # Set data array to be one longer than number of points, # with a dummy vertex added at the end. This is to ensure @@ -1284,7 +1300,9 @@ class VMobject(Mobject): lst.append(submob.data[stroke_names][-1:]) shader_wrappers = [ - self.back_stroke_shader_wrapper.read_in(back_stroke_data), + self.back_stroke_shader_wrapper.read_in( + [*back_stroke_datas, *fill_border_datas] + ), self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), self.stroke_shader_wrapper.read_in(stroke_datas), ] diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 643099a3..8d261e49 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -150,8 +150,6 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, #version 330 uniform sampler2D Texture; - uniform float v_nudge; - uniform float h_nudge; uniform vec3 null_rgb; in vec2 v_textcoord; @@ -160,17 +158,7 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, const float MIN_DIST_TO_NULL = 0.2; void main() { - // Apply poor man's anti-aliasing - vec2 nudges[4] = vec2[4]( - vec2(0, 0), - vec2(0, h_nudge), - vec2(v_nudge, 0), - vec2(v_nudge, h_nudge) - ); - color = vec4(0.0); - for(int i = 0; i < 4; i++){ - color += 0.25 * texture(Texture, v_textcoord + nudges[i]); - } + color = texture(Texture, v_textcoord); if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard; // Un-blend from the null value @@ -182,9 +170,6 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, ) simple_program['Texture'].value = get_texture_id(texture) - # Half pixel width/height - simple_program['h_nudge'].value = 0.5 / size[0] - simple_program['v_nudge'].value = 0.5 / size[1] simple_program['null_rgb'].value = null_rgb verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) From ce5d0b61f9ba86edd7f809815230772bcb7bc23d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 16:57:22 -0800 Subject: [PATCH 33/36] Add back accidentally deleted reverse_points code --- manimlib/mobject/types/vectorized_mobject.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 1e883574..f2c5e452 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1190,6 +1190,9 @@ class VMobject(Mobject): continue inner_ends = mob.get_subpath_end_indices()[:-1] mob.data["point"][inner_ends + 1] = mob.data["point"][inner_ends + 2] + mob.data["unit_normal"] *= -1 + super().reverse_points() + return self @triggers_refreshed_triangulation def set_data(self, data: np.ndarray): From 8ecfc2b2cf7af8ffc7ba0801e324b28e561ba86f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 17:08:22 -0800 Subject: [PATCH 34/36] add shaders to universal imports --- manimlib/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/__init__.py b/manimlib/__init__.py index 063cfa62..ca4d4185 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -73,6 +73,7 @@ from manimlib.utils.iterables import * from manimlib.utils.paths import * from manimlib.utils.rate_functions import * from manimlib.utils.simple_functions import * +from manimlib.utils.shaders import * from manimlib.utils.sounds import * from manimlib.utils.space_ops import * from manimlib.utils.tex import * From 38abef88713b09daed26b42a8dc99c10ffceb053 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 19:27:23 -0800 Subject: [PATCH 35/36] Fix ShaderWrapper.init_textures --- manimlib/shader_wrapper.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 38f1f606..434f1802 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -80,10 +80,11 @@ class ShaderWrapper(object): 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 + names_to_ids = { + name: get_texture_id(image_path_to_texture(path, self.ctx)) + for name, path in texture_paths.items() + } + self.update_program_uniforms(names_to_ids) def init_vao(self): self.vbo = None From 047128a6632a82d484379a76a7949885ca16af87 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 27 Jan 2023 19:27:42 -0800 Subject: [PATCH 36/36] Make sure shader_wrapper inherits depth test --- manimlib/mobject/mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 18efb62a..ebe82af6 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1943,6 +1943,7 @@ class Mobject(object): shader_wrapper.generate_vao() self._data_has_changed = False for shader_wrapper in self.shader_wrappers: + shader_wrapper.depth_test = self.depth_test shader_wrapper.update_program_uniforms(self.get_uniforms()) shader_wrapper.update_program_uniforms(camera_uniforms, universal=True) shader_wrapper.pre_render()