From 27f397e0a6c6a884fb9f7cb7193fbfe3c33feee0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 7 Mar 2024 16:47:24 -0300 Subject: [PATCH 01/59] Have stroke width change continuously with fixed_in_frame status --- manimlib/shaders/quadratic_bezier_stroke/vert.glsl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index f26a1261..ea290bfa 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -21,9 +21,7 @@ const float STROKE_WIDTH_CONVERSION = 0.01; void main(){ verts = point; v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width; - if(!bool(is_fixed_in_frame)){ - v_stroke_width *= frame_scale; - } + v_stroke_width *= mix(frame_scale, 1, is_fixed_in_frame); v_joint_product = joint_product; v_color = stroke_rgba; } \ No newline at end of file From fa99eafe2b50efa623df5386be11653352c575ab Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 16 Mar 2024 11:10:42 -0300 Subject: [PATCH 02/59] Account for rgba case in point_to_rgb --- manimlib/mobject/types/image_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/types/image_mobject.py b/manimlib/mobject/types/image_mobject.py index b49872de..605c2aeb 100644 --- a/manimlib/mobject/types/image_mobject.py +++ b/manimlib/mobject/types/image_mobject.py @@ -71,5 +71,5 @@ class ImageMobject(Mobject): rgb = self.image.getpixel(( int((pw - 1) * x_alpha), int((ph - 1) * y_alpha), - )) + ))[:3] return np.array(rgb) / 255 From 7db69e32aa6a0763aa2053dbdf95a33b18d43919 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 16 Mar 2024 11:10:52 -0300 Subject: [PATCH 03/59] Update input type to LaggedStartMap --- manimlib/animation/composition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index d23d90cb..a21f076d 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -168,7 +168,7 @@ class LaggedStart(AnimationGroup): class LaggedStartMap(LaggedStart): def __init__( self, - anim_func: Callable[[Mobject], Animation], + anim_func: Callable[[Mobject], Animation] | Animation, group: Mobject, run_time: float = 2.0, lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, From b26feb70458cd212b911ea152b08391ca7c032b1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 09:59:29 -0300 Subject: [PATCH 04/59] Adjust Underline configuration --- manimlib/mobject/shape_matchers.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index d42bde90..c201ad14 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -118,17 +118,13 @@ class Underline(Line): mobject: Mobject, buff: float = SMALL_BUFF, stroke_color=WHITE, - stroke_width: float | Sequence[float] = [0, 3, 3, 0], + stroke_width: float | Sequence[float] = [0, 2, 3, 3, 2, 0], stretch_factor=1.2, **kwargs ): - super().__init__( - LEFT, RIGHT, - stroke_color=stroke_color, - stroke_width=stroke_width, - **kwargs - ) - self.insert_n_curves(30) + super().__init__(LEFT, RIGHT, **kwargs) + if not isinstance(stroke_width, (float, int)): + self.insert_n_curves(len(stroke_width) - 2) self.set_stroke(stroke_color, stroke_width) self.set_width(mobject.get_width() * stretch_factor) self.next_to(mobject, DOWN, buff=buff) From 6b3834739c3a687f434a8d7070c2c3e05f1bb465 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 10:00:11 -0300 Subject: [PATCH 05/59] Undo redundant previous tweak to LaggedStartMap input type --- manimlib/animation/composition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index a21f076d..d23d90cb 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -168,7 +168,7 @@ class LaggedStart(AnimationGroup): class LaggedStartMap(LaggedStart): def __init__( self, - anim_func: Callable[[Mobject], Animation] | Animation, + anim_func: Callable[[Mobject], Animation], group: Mobject, run_time: float = 2.0, lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, From 7e6a37d4996ff43f673d63ba088d777896fa3f2e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 10:03:41 -0300 Subject: [PATCH 06/59] Typo fix: make_number_changable -> make_number_changeable --- example_scenes.py | 8 ++++---- manimlib/mobject/svg/tex_mobject.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index daea02eb..1f4dfeb2 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -289,7 +289,7 @@ class UpdatersExample(Scene): brace = always_redraw(Brace, square, UP) label = TexText("Width = 0.00") - number = label.make_number_changable("0.00") + number = label.make_number_changeable("0.00") # This ensures that the method deicmal.next_to(square) # is called on every frame @@ -515,7 +515,7 @@ class TexAndNumbersExample(Scene): # on them. tex = Tex("x^2 + y^2 = 4.00") tex.next_to(axes, UP, buff=0.5) - value = tex.make_number_changable("4.00") + value = tex.make_number_changeable("4.00") # This will tie the right hand side of our equation to @@ -537,10 +537,10 @@ class TexAndNumbersExample(Scene): rate_func=there_and_back, ) - # By default, tex.make_number_changable replaces the first occurance + # By default, tex.make_number_changeable replaces the first occurance # of the number,but by passing replace_all=True it replaces all and # returns a group of the results - exponents = tex.make_number_changable("2", replace_all=True) + exponents = tex.make_number_changeable("2", replace_all=True) self.play( LaggedStartMap( FlashAround, exponents, diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 144ca66e..54da7fa6 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -231,7 +231,7 @@ class Tex(StringMobject): )) return re.findall(pattern, self.string) - def make_number_changable( + def make_number_changeable( self, value: float | int | str, index: int = 0, @@ -241,7 +241,7 @@ class Tex(StringMobject): substr = str(value) parts = self.select_parts(substr) if len(parts) == 0: - log.warning(f"{value} not found in Tex.make_number_changable call") + log.warning(f"{value} not found in Tex.make_number_changeable call") return VMobject() if index > len(parts) - 1: log.warning(f"Requested {index}th occurance of {value}, but only {len(parts)} exist") From 223d671eea295401f993a968628b11242ff5a5fe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 14:34:39 -0300 Subject: [PATCH 07/59] Remove redundancy --- manimlib/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/__init__.py b/manimlib/__init__.py index ca4d4185..8467f980 100644 --- a/manimlib/__init__.py +++ b/manimlib/__init__.py @@ -43,7 +43,6 @@ from manimlib.mobject.probability import * from manimlib.mobject.shape_matchers import * from manimlib.mobject.svg.brace import * from manimlib.mobject.svg.drawings import * -from manimlib.mobject.svg.tex_mobject import * from manimlib.mobject.svg.string_mobject import * from manimlib.mobject.svg.svg_mobject import * from manimlib.mobject.svg.special_tex import * From ec42326618125d95dffc7c9c4654bd9d559e7eeb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 14:35:33 -0300 Subject: [PATCH 08/59] Fix remover=True case for FadeTransform --- manimlib/animation/fading.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index 54f183c3..dee3fe89 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -134,7 +134,8 @@ class FadeTransform(Transform): Animation.clean_up_from_scene(self, scene) scene.remove(self.mobject) self.mobject[0].restore() - scene.add(self.to_add_on_completion) + if not self.remover: + scene.add(self.to_add_on_completion) class FadeTransformPieces(FadeTransform): From 0509e824c63af8be0681fbb96dbf8ac288388066 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 14:36:06 -0300 Subject: [PATCH 09/59] Have border_width default to 0 for lower opacity --- manimlib/mobject/types/vectorized_mobject.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 7286ff6f..41567f42 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -190,9 +190,10 @@ class VMobject(Mobject): recurse: bool = True ) -> Self: 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 + if border_width is None: + border_width = 0 if self.get_fill_opacity() < 1 else 0.5 + for mob in self.get_family(recurse): + mob.data["fill_border_width"] = border_width self.note_changed_fill() return self From 1d6aa47933833442dd57f949b892a48a187355af Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 21 Mar 2024 14:36:17 -0300 Subject: [PATCH 10/59] Reimplement SpeechBubble and ThoughtBubble --- manimlib/mobject/svg/drawings.py | 217 ++++++++++++++++++++++--------- 1 file changed, 159 insertions(+), 58 deletions(-) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index f620b47a..f4d3f8d3 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -2,6 +2,7 @@ from __future__ import annotations import numpy as np import itertools as it +import random from manimlib.animation.composition import AnimationGroup from manimlib.animation.rotation import Rotating @@ -24,6 +25,7 @@ from manimlib.constants import LEFT from manimlib.constants import LEFT from manimlib.constants import MED_LARGE_BUFF from manimlib.constants import MED_SMALL_BUFF +from manimlib.constants import LARGE_BUFF from manimlib.constants import ORIGIN from manimlib.constants import OUT from manimlib.constants import PI @@ -41,6 +43,7 @@ from manimlib.constants import WHITE from manimlib.constants import YELLOW from manimlib.constants import TAU from manimlib.mobject.boolean_ops import Difference +from manimlib.mobject.boolean_ops import Union from manimlib.mobject.geometry import Arc from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Dot @@ -51,6 +54,7 @@ from manimlib.mobject.geometry import Square from manimlib.mobject.geometry import AnnularSector from manimlib.mobject.mobject import Mobject from manimlib.mobject.numbers import Integer +from manimlib.mobject.shape_matchers import SurroundingRectangle from manimlib.mobject.svg.svg_mobject import SVGMobject from manimlib.mobject.svg.tex_mobject import Tex from manimlib.mobject.svg.tex_mobject import TexText @@ -59,9 +63,13 @@ from manimlib.mobject.three_dimensions import Prismify from manimlib.mobject.three_dimensions import VCube from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.svg.text_mobject import Text +from manimlib.utils.bezier import interpolate +from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.rate_functions import linear from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import compass_directions +from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import midpoint from manimlib.utils.space_ops import rotate_vector @@ -344,66 +352,76 @@ class ClockPassesTime(AnimationGroup): ) -class Bubble(SVGMobject): +class Bubble(VGroup): file_name: str = "Bubbles_speech.svg" + bubble_center_adjustment_factor = 0.125 def __init__( self, + content: str | VMobject | None = None, + buff: float = 1.0, + filler_shape: Tuple[float, float] = (3.0, 2.0), + pin_point: Vect3 | None = None, direction: Vect3 = LEFT, - center_point: Vect3 = ORIGIN, - content_scale_factor: float = 0.7, - height: float = 4.0, - width: float = 8.0, - max_height: float | None = None, - max_width: float | None = None, - bubble_center_adjustment_factor: float = 0.125, + add_content: bool = True, fill_color: ManimColor = BLACK, fill_opacity: float = 0.8, stroke_color: ManimColor = WHITE, stroke_width: float = 3.0, **kwargs ): - self.direction = LEFT # Possibly updated below by self.flip() - self.bubble_center_adjustment_factor = bubble_center_adjustment_factor - self.content_scale_factor = content_scale_factor + super().__init__(**kwargs) + self.direction = direction - super().__init__( - fill_color=fill_color, - fill_opacity=fill_opacity, - stroke_color=stroke_color, - stroke_width=stroke_width, - **kwargs - ) + if content is None: + content = Rectangle(*filler_shape) + content.set_fill(opacity=0) + content.set_stroke(width=0) + elif isinstance(content, str): + content = Text(content) + self.content = content - self.center() - self.set_height(height, stretch=True) - self.set_width(width, stretch=True) - if max_height: - self.set_max_height(max_height) - if max_width: - self.set_max_width(max_width) + self.body = self.get_body(content, direction, buff) + self.body.set_fill(fill_color, fill_opacity) + self.body.set_stroke(stroke_color, stroke_width) + self.add(self.body) + + if add_content: + self.add(self.content) + + if pin_point is not None: + self.pin_to(pin_point) + + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + body = SVGMobject(self.file_name) if direction[0] > 0: - self.flip() - - self.content = VMobject() + body.flip() + # Resize + width = content.get_width() + height = content.get_height() + target_width = width + min(buff, height) + target_height = 1.35 * (height + buff) # Magic number? + body.set_shape(target_width, target_height) + body.move_to(content) + body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN) + return body def get_tip(self): - # TODO, find a better way - return self.get_corner(DOWN + self.direction) - 0.6 * self.direction + return self.get_corner(DOWN + self.direction) def get_bubble_center(self): factor = self.bubble_center_adjustment_factor return self.get_center() + factor * self.get_height() * UP def move_tip_to(self, point): - mover = VGroup(self) - if self.content is not None: - mover.add(self.content) - mover.shift(point - self.get_tip()) + self.shift(point - self.get_tip()) return self - def flip(self, axis=UP): - super().flip(axis=axis) + def flip(self, axis=UP, only_body=True, **kwargs): + if only_body: + self.body.flip(axis=axis, **kwargs) + else: + super().flip(axis=axis, **kwargs) if abs(axis[1]) > 0: self.direction = -np.array(self.direction) return self @@ -418,9 +436,9 @@ class Bubble(SVGMobject): self.move_tip_to(mob_center + vector_from_center) return self - def position_mobject_inside(self, mobject): - mobject.set_max_width(self.content_scale_factor * self.get_width()) - mobject.set_max_height(self.content_scale_factor * self.get_height() / 1.5) + def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF): + mobject.set_max_width(self.body.get_width() - 2 * buff) + mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff) mobject.shift(self.get_bubble_center() - mobject.get_center()) return mobject @@ -429,26 +447,110 @@ class Bubble(SVGMobject): self.content = mobject return self.content - def write(self, *text): - self.add_content(TexText(*text)) + def write(self, text): + self.add_content(Text(text)) return self - def resize_to_content(self, buff=0.75): - width = self.content.get_width() - height = self.content.get_height() - target_width = width + min(buff, height) - target_height = 1.35 * (self.content.get_height() + buff) - tip_point = self.get_tip() - self.stretch_to_fit_width(target_width, about_point=tip_point) - self.stretch_to_fit_height(target_height, about_point=tip_point) - self.position_mobject_inside(self.content) + def resize_to_content(self, buff=1.0): # TODO + self.body.match_points(self.get_body( + self.content, self.direction, buff + )) def clear(self): - self.add_content(VMobject()) + self.remove(self.content) return self class SpeechBubble(Bubble): + def __init__( + self, + content: str | VMobject | None = None, + buff: float = MED_SMALL_BUFF, + filler_shape: Tuple[float, float] = (2.0, 1.0), + stem_height_to_bubble_height: float = 0.5, + stem_top_x_props: Tuple[float, float] = (0.2, 0.3), + **kwargs + ): + self.stem_height_to_bubble_height = stem_height_to_bubble_height + self.stem_top_x_props = stem_top_x_props + super().__init__(content, buff, filler_shape, **kwargs) + + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + rect = SurroundingRectangle(content, buff=buff) + rect.round_corners() + lp = rect.get_corner(DL) + rp = rect.get_corner(DR) + stem_height = self.stem_height_to_bubble_height * rect.get_height() + low_prop, high_prop = self.stem_top_x_props + triangle = Polygon( + interpolate(lp, rp, low_prop), + interpolate(lp, rp, high_prop), + lp + stem_height * DOWN, + ) + result = Union(rect, triangle) + result.insert_n_curves(20) + if direction[0] > 0: + result.flip() + + return result + + +class ThoughtBubble(Bubble): + def __init__( + self, + content: str | VMobject | None = None, + buff: float = SMALL_BUFF, + filler_shape: Tuple[float, float] = (2.0, 1.0), + bulge_radius: float = 0.35, + bulge_overlap: float = 0.25, + noise_factor: float = 0.1, + circle_radii: list[float] = [0.1, 0.15, 0.2], + **kwargs + ): + self.bulge_radius = bulge_radius + self.bulge_overlap = bulge_overlap + self.noise_factor = noise_factor + self.circle_radii = circle_radii + super().__init__(content, buff, filler_shape, **kwargs) + + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + rect = SurroundingRectangle(content, buff) + perimeter = rect.get_arc_length() + radius = self.bulge_radius + step = (1 - self.bulge_overlap) * (2 * radius) + nf = self.noise_factor + corners = [rect.get_corner(v) for v in [DL, UL, UR, DR]] + points = [] + for c1, c2 in adjacent_pairs(corners): + n_alphas = int(get_norm(c1 - c2) / step) + 1 + for alpha in np.linspace(0, 1, n_alphas): + points.append(interpolate( + c1, c2, alpha + nf * (step / n_alphas) * (random.random() - 0.5) + )) + + cloud = Union(rect, *( + # Add bulges + Circle(radius=radius * (1 + nf * random.random())).move_to(point) + for point in points + )) + cloud.set_stroke(WHITE, 2) + + circles = VGroup(Circle(radius=radius) for radius in self.circle_radii) + circ_buff = 0.25 * self.circle_radii[0] + circles.arrange(UR, buff=circ_buff) + circles[1].shift(circ_buff * DR) + circles.next_to(cloud, DOWN, 4 * circ_buff, aligned_edge=LEFT) + circles.set_stroke(WHITE, 2) + + result = VGroup(*circles, cloud) + + if direction[0] > 0: + result.flip() + + return result + + +class OldSpeechBubble(Bubble): file_name: str = "Bubbles_speech.svg" @@ -456,17 +558,16 @@ class DoubleSpeechBubble(Bubble): file_name: str = "Bubbles_double_speech.svg" -class ThoughtBubble(Bubble): +class OldThoughtBubble(Bubble): file_name: str = "Bubbles_thought.svg" - def __init__(self, **kwargs): - Bubble.__init__(self, **kwargs) - self.submobjects.sort( - key=lambda m: m.get_bottom()[1] - ) + def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject: + body = super().get_body(content, direction, buff) + body.sort(lambda p: p[1]) + return body def make_green_screen(self): - self.submobjects[-1].set_fill(GREEN_SCREEN, opacity=1) + self.body[-1].set_fill(GREEN_SCREEN, opacity=1) return self From 7565e936fa32579d610890f4a40977cd43c768e0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2024 19:10:16 -0300 Subject: [PATCH 11/59] Better bubble flipping --- manimlib/mobject/svg/drawings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index f4d3f8d3..198b3560 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -418,10 +418,10 @@ class Bubble(VGroup): return self def flip(self, axis=UP, only_body=True, **kwargs): + super().flip(axis=axis, **kwargs) if only_body: - self.body.flip(axis=axis, **kwargs) - else: - super().flip(axis=axis, **kwargs) + # Flip in place, don't use kwargs + self.content.flip(axis=axis) if abs(axis[1]) > 0: self.direction = -np.array(self.direction) return self From 920f2407e03d8416ba56fb2f333ebc7c9c115fea Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2024 19:10:30 -0300 Subject: [PATCH 12/59] Revert default underline stroke width --- manimlib/mobject/shape_matchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index c201ad14..72942e14 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -118,7 +118,7 @@ class Underline(Line): mobject: Mobject, buff: float = SMALL_BUFF, stroke_color=WHITE, - stroke_width: float | Sequence[float] = [0, 2, 3, 3, 2, 0], + stroke_width: float | Sequence[float] = [0, 3, 3, 0], stretch_factor=1.2, **kwargs ): From f5d1a9c449bb0bbb878888aa1753fe6adfaf2398 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Mar 2024 19:10:42 -0300 Subject: [PATCH 13/59] Keep track of original float matrix in DecimalMatrix --- manimlib/mobject/matrix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/matrix.py b/manimlib/mobject/matrix.py index 9b93381c..cb5991bf 100644 --- a/manimlib/mobject/matrix.py +++ b/manimlib/mobject/matrix.py @@ -222,6 +222,7 @@ class DecimalMatrix(Matrix): decimal_config: dict = dict(), **config ): + self.float_matrix = matrix super().__init__( matrix, element_config=dict( From 772a3283023ed8965dee5e759912278eeedcbaa9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 12 Apr 2024 21:52:08 -0400 Subject: [PATCH 14/59] Have FadeTransform target match all uniforms of source --- manimlib/animation/fading.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/animation/fading.py b/manimlib/animation/fading.py index dee3fe89..207de90c 100644 --- a/manimlib/animation/fading.py +++ b/manimlib/animation/fading.py @@ -118,6 +118,7 @@ class FadeTransform(Transform): def ghost_to(self, source: Mobject, target: Mobject) -> None: source.replace(target, stretch=self.stretch, dim_to_match=self.dim_to_match) + source.set_uniform(**target.get_uniforms()) source.set_opacity(0) def get_all_mobjects(self) -> list[Mobject]: From 4feb831a11054d32f5fc7e4aa294f8b8387f1633 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 24 Jun 2024 15:21:09 -0700 Subject: [PATCH 15/59] Pass group parameter in LaggedStartMap -> AnimationGroup --- manimlib/animation/composition.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index d23d90cb..a0a7255c 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -180,4 +180,5 @@ class LaggedStartMap(LaggedStart): *(anim_func(submob, **anim_kwargs) for submob in group), run_time=run_time, lag_ratio=lag_ratio, + group=group ) From a07ccf4aca1b6400d559c956c3b4932f5c256b47 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 24 Jun 2024 15:22:22 -0700 Subject: [PATCH 16/59] Include *args, **kwargs in embed shell event pre_cell and post_cell functions --- manimlib/scene/scene.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a80d1866..f8ed24bb 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -213,7 +213,8 @@ class Scene(object): show_animation_progress: bool = False, ) -> None: if not self.preview: - return # Embed is only relevant with a preview + # Embed is only relevant with a preview + return self.stop_skipping() self.update_frame() self.save_state() @@ -260,11 +261,11 @@ class Scene(object): # namespace, since this is just a shell session anyway. shell.events.register( "pre_run_cell", - lambda: shell.user_global_ns.update(shell.user_ns) + lambda *args, **kwargs: shell.user_global_ns.update(shell.user_ns) ) # Operation to run after each ipython command - def post_cell_func(): + def post_cell_func(*args, **kwargs): if not self.is_window_closing(): self.update_frame(dt=0, ignore_skipping=True) self.save_state() @@ -273,7 +274,7 @@ class Scene(object): # Flash border, and potentially play sound, on exceptions def custom_exc(shell, etype, evalue, tb, tb_offset=None): - # still show the error don't just swallow it + # Show the error don't just swallow it shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) if self.embed_error_sound: os.system("printf '\a'") From 79c89ad34d1d479afb8268328d43c2c910eeed9c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:23:16 +0200 Subject: [PATCH 17/59] Remove (outdated) background rectangle arg from example scene --- example_scenes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index 1f4dfeb2..80111134 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -26,7 +26,7 @@ class OpeningManimExample(Scene): matrix = [[1, 1], [0, 1]] linear_transform_words = VGroup( Text("This is what the matrix"), - IntegerMatrix(matrix, include_background_rectangle=True), + IntegerMatrix(matrix), Text("looks like") ) linear_transform_words.arrange(RIGHT) @@ -251,7 +251,7 @@ class TexIndexing(Scene): self.play(FlashAround(part)) self.wait() self.play(FadeOut(equation)) - + # Indexing by substrings like this may not work when # the order in which Latex draws symbols does not match # the order in which they show up in the string. From d1314e5a3c95e52de28992d4da37718a90a2732a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:23:41 +0200 Subject: [PATCH 18/59] Catch screeninfo.ScreenInfoError error --- manimlib/config.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index 1de7bbd7..a6a8f62b 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -6,7 +6,7 @@ import colour import importlib import inspect import os -from screeninfo import get_monitors +import screeninfo import sys import yaml @@ -433,7 +433,10 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict: def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict: # Default to making window half the screen size # but make it full screen if -f is passed in - monitors = get_monitors() + try: + monitors = screeninfo.get_monitors() + except screeninfo.ScreenInfoError: + pass mon_index = custom_config["window_monitor"] monitor = monitors[min(mon_index, len(monitors) - 1)] aspect_ratio = camera_config["pixel_width"] / camera_config["pixel_height"] From 57d4732ef12f88744c8abf81e3149bb9c9affee1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:23:59 +0200 Subject: [PATCH 19/59] Remove unused lines --- manimlib/mobject/coordinate_systems.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index fcd11612..0502b19e 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -603,9 +603,6 @@ class ThreeDAxes(Axes): **kwargs ) -> ParametricSurface: surface = ParametricSurface(func, color=color, opacity=opacity, **kwargs) - xu = self.x_axis.get_unit_size() - yu = self.y_axis.get_unit_size() - zu = self.z_axis.get_unit_size() axes = [self.x_axis, self.y_axis, self.z_axis] for dim, axis in zip(range(3), axes): surface.stretch(axis.get_unit_size(), dim, about_point=ORIGIN) From a7765dcac3e9161c948e11e2f4f8af340d51bda6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:24:25 +0200 Subject: [PATCH 20/59] Change copy frame position command --- manimlib/scene/interactive_scene.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 683e6f8a..7c32f2f6 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -48,6 +48,7 @@ RESIZE_KEY = 't' COLOR_KEY = 'c' INFORMATION_KEY = 'i' CURSOR_KEY = 'k' +COPY_FRAME_POSITION_KEY = 'p' # Note, a lot of the functionality here is still buggy and very much a work in progress. @@ -504,7 +505,7 @@ class InteractiveScene(Scene): self.toggle_selection_mode() elif char == "s" and modifiers == COMMAND_MODIFIER: self.save_selection_to_file() - elif char == PAN_3D_KEY and modifiers == COMMAND_MODIFIER: + elif char == "d" and modifiers == SHIFT_MODIFIER: self.copy_frame_positioning() elif symbol in ARROW_SYMBOLS: self.nudge_selection( From c1efd1490459c1bfe01e6892a021601f5ad91749 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:24:50 +0200 Subject: [PATCH 21/59] Add touch and notouch to embed vocabulary --- manimlib/scene/scene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index f8ed24bb..b4e40ae0 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -240,6 +240,8 @@ class Scene(object): i2g=self.i2g, i2m=self.i2m, checkpoint_paste=self.checkpoint_paste, + touch=lambda: shell.enable_gui("manim"), + notouch=lambda: shell.enable_gui(None), ) # Enables gui interactions during the embed From a105216a4776d458f998f58a0ec611af63f3ab79 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:31:26 +0200 Subject: [PATCH 22/59] Small clean up --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 3297d62b..937674bc 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -163,7 +163,6 @@ void main() { vec3 v01 = normalize(p1 - p0); vec3 v12 = normalize(p2 - p1); - vec4 jp1 = normalized_joint_product(v_joint_product[1]); is_linear = float(jp1.w > COS_THRESHOLD); @@ -189,7 +188,7 @@ void main() { float stroke_width = v_stroke_width[i / 2]; if(bool(is_linear)){ - float sign = vec2(-1, 1)[i % 2]; + float sign = (i % 2 == 0 ? -1 : 1); // In this case, we only really care about // the v coordinate uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw)); From bbc89d13e99587cd0b4fd4df1e51f5b910634617 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:32:08 +0200 Subject: [PATCH 23/59] Don't save state after each embed cell call --- manimlib/scene/scene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index b4e40ae0..6035c0a2 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -270,7 +270,6 @@ class Scene(object): def post_cell_func(*args, **kwargs): if not self.is_window_closing(): self.update_frame(dt=0, ignore_skipping=True) - self.save_state() shell.events.register("post_run_cell", post_cell_func) From 5aeb457bb1ae9c0a86a20279eccbe1b69e2d7e33 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:32:17 +0200 Subject: [PATCH 24/59] Hot fix for Traicing Tail --- manimlib/mobject/changing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index 5206812d..e348b7c1 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -167,3 +167,4 @@ class TracingTail(TracedPath): stroke_color=stroke_color, **kwargs ) + self.add_updater(lambda m: m.set_stroke(width=stroke_width, opacity=stroke_opacity)) From 3ea8393e9a0f8dbf8554294567b6fbec71660d1c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 15:51:06 -0400 Subject: [PATCH 25/59] First pass at a polyline implementation for stroke --- .../shaders/quadratic_bezier_stroke/frag.glsl | 56 +----- .../shaders/quadratic_bezier_stroke/geom.glsl | 186 +++++++++--------- .../shaders/quadratic_bezier_stroke/vert.glsl | 3 +- 3 files changed, 94 insertions(+), 151 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index fb9a483e..a8170ec5 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -1,66 +1,18 @@ #version 330 -in vec2 uv_coords; - +in float signed_dist_to_curve; in float uv_stroke_width; in float uv_anti_alias_width; in vec4 color; -in float is_linear; - out vec4 frag_color; -const float QUICK_DIST_WIDTH = 0.2; - -float dist_to_curve(){ - // In the linear case, the curve will have - // been set to equal the x axis - if(bool(is_linear)) return abs(uv_coords.y); - - // Otherwise, find the distance from uv_coords to the curve y = x^2 - float x0 = uv_coords.x; - float y0 = uv_coords.y; - - // This is a quick approximation for computing - // the distance to the curve. - // Evaluate F(x, y) = y - x^2 - // divide by its gradient's magnitude - float Fxy = y0 - x0 * x0; - float approx_dist = abs(Fxy) * inversesqrt(1.0 + 4 * x0 * x0); - if(approx_dist < QUICK_DIST_WIDTH) return approx_dist; - - // Otherwise, solve for the minimal distance. - // The distance squared between (x0, y0) and a point (x, x^2) looks like - // - // (x0 - x)^2 + (y0 - x^2)^2 = x^4 + (1 - 2y0)x^2 - 2x0 * x + (x0^2 + y0^2) - // - // Setting the derivative equal to zero (and rescaling) looks like - // - // x^3 + (0.5 - y0) * x - 0.5 * x0 = 0 - // - // Adapted from https://www.shadertoy.com/view/ws3GD7 - x0 = abs(x0); - float p = (0.5 - y0) / 3.0; // p / 3 in usual Cardano's formula notation - float q = 0.25 * x0; // -q / 2 in usual Cardano's formula notation - float disc = q*q + p*p*p; - float r = sqrt(abs(disc)); - - float x = (disc > 0.0) ? - // 1 root - pow(q + r, 1.0 / 3.0) + pow(abs(q - r), 1.0 / 3.0) * sign(-p) : - // 3 roots - 2.0 * cos(atan(r, q) / 3.0) * sqrt(-p); - - return length(vec2(x0 - x, y0 - x * x)); -} - - void main() { if (uv_stroke_width == 0) discard; frag_color = color; // sdf for the region around the curve we wish to color. - float signed_dist = dist_to_curve() - 0.5 * uv_stroke_width; - - frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width); + float signed_dist_to_region = abs(signed_dist_to_curve) - 0.5 * uv_stroke_width; + frag_color.a *= smoothstep(1.0, 0.0, signed_dist_to_region / uv_anti_alias_width); + frag_color.a += 0.2; // undo } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 937674bc..7c4a075d 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -1,7 +1,7 @@ #version 330 layout (triangles) in; -layout (triangle_strip, max_vertices = 6) out; +layout (triangle_strip, max_vertices = 32) out; // Related to MAX_STEPS below uniform float anti_alias_width; uniform float flat_stroke; @@ -17,10 +17,7 @@ in vec4 v_color[3]; out vec4 color; out float uv_stroke_width; out float uv_anti_alias_width; - -out float is_linear; - -out vec2 uv_coords; +out float signed_dist_to_curve; // Codes for joint types const int NO_JOINT = 0; @@ -31,7 +28,8 @@ const int MITER_JOINT = 3; // When the cosine of the angle between // two vectors is larger than this, we // consider them aligned -const float COS_THRESHOLD = 0.99; +const float COS_THRESHOLD = 0.999; +const int MAX_STEPS = 16; vec3 unit_normal = vec3(0.0, 0.0, 1.0); @@ -83,73 +81,68 @@ void create_joint( changing_c1 = static_c1 + shift * unit_tan; } -vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw){ +vec3 get_perp(vec4 joint_product, vec3 point, vec3 tangent){ /* 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.75) buff *= 4 * (joint_product.w + 1.0); + float mult = 1.0; + if(joint_product.w < -0.75) mult *= 4 * (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)); + return mult * normalize(cross(normal, tangent)); }else{ - return buff * normalize(cross(camera_position - point, tangent)); + return mult * 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 -// to control points so that the passage of vert data to -// frag shaders is most natural. -void get_corners( - // Control points for a bezier curve - vec3 p0, - vec3 p1, - vec3 p2, - // Unit tangent vectors at p0 and p2 - vec3 v01, - vec3 v12, - // Anti-alias width - float aaw, - out vec3 corners[6] + +vec3 point_on_curve(float t){ + return verts[0] + 2 * (verts[1] - verts[0]) * t + (verts[0] - 2 * verts[1] + verts[2]) * t * t; +} + + +vec3 tangent_on_curve(float t){ + return 2 * (verts[1] + -verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t; +} + + +void emit_point_with_width( + vec3 point, + vec3 tangent, + vec4 joint_product, + float width, + vec4 joint_color, + float aaw ){ - 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); + vec3 unit_tan = normalize(tangent); + vec4 njp = normalized_joint_product(joint_product); + float buff = 0.5 * width + aaw; + vec3 perp = buff * get_perp(njp, point, unit_tan); + + vec3 corners[2] = vec3[2](point + perp, point - perp); + create_joint( + njp, unit_tan, length(perp), + corners[0], corners[0], + corners[1], corners[1] + ); + + color = finalize_color(joint_color, point, unit_normal); + uv_anti_alias_width = aaw; + uv_stroke_width = width; + + // Emit two corners + for(int i = 0; i < 2; i++){ + float sign = i % 2 == 0 ? -1 : 1; + signed_dist_to_curve = sign * buff; + emit_gl_Position(corners[i]); + EmitVertex(); } - // The order of corners should be for a triangle_strip. - vec3 c0 = p0 + p0_perp; - vec3 c1 = p0 - p0_perp; - vec3 c2 = p1 + p1_perp; - vec3 c3 = p1 - p1_perp; - vec3 c4 = p2 + p2_perp; - vec3 c5 = p2 - p2_perp; - // Move the inner middle control point to make - // room for the curve - // 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, 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); } void main() { @@ -157,52 +150,51 @@ void main() { // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - vec3 p0 = verts[0]; - vec3 p1 = verts[1]; - vec3 p2 = verts[2]; - vec3 v01 = normalize(p1 - p0); - vec3 v12 = normalize(p2 - p1); - vec4 jp1 = normalized_joint_product(v_joint_product[1]); - is_linear = float(jp1.w > COS_THRESHOLD); + bool is_linear = jp1.w > COS_THRESHOLD; // TODO, something with this - // 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 - // the case of a linear curve just put it on the x-axis - mat4 xyz_to_uv; - float uv_scale_factor; - if(!bool(is_linear)){ - bool too_steep; - xyz_to_uv = get_xyz_to_uv(p0, p1, p2, 2.0, too_steep); - is_linear = float(too_steep); - uv_scale_factor = length(xyz_to_uv[0].xyz); + // Compute subdivision + int n_steps; + if (is_linear){ + n_steps = 2; + }else{ + n_steps = MAX_STEPS; // TODO } + float subdivision[MAX_STEPS]; + vec3 points[MAX_STEPS]; + for(int i = 0; i < MAX_STEPS; i++){ + if (i >= n_steps) break; + subdivision[i] = float(i) / (n_steps - 1); + points[i] = point_on_curve(subdivision[i]); + } + + // Compute joint products + vec4 joint_products[MAX_STEPS]; + joint_products[0] = v_joint_product[0]; + joint_products[0].xyz *= -1; + joint_products[n_steps - 1] = v_joint_product[2]; + for (int i = 1; i < MAX_STEPS; i++){ + if (i >= n_steps - 1) break; + vec3 v1 = points[i] - points[i - 1]; + vec3 v2 = points[i + 1] - points[i]; + joint_products[i].xyz = cross(v1, v2); + joint_products[i].w = dot(v1, v2); + } + + // Intermediate points float scaled_aaw = anti_alias_width * pixel_size; - vec3 corners[6]; - get_corners(p0, p1, p2, v01, v12, scaled_aaw, corners); - - // Emit each corner - float max_sw = max(v_stroke_width[0], v_stroke_width[2]); - for(int i = 0; i < 6; i++){ - float stroke_width = v_stroke_width[i / 2]; - - if(bool(is_linear)){ - float sign = (i % 2 == 0 ? -1 : 1); - // In this case, we only really care about - // the v coordinate - uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw)); - uv_anti_alias_width = scaled_aaw; - uv_stroke_width = stroke_width; - }else{ - uv_coords = (xyz_to_uv * vec4(corners[i], 1.0)).xy; - uv_stroke_width = uv_scale_factor * stroke_width; - uv_anti_alias_width = uv_scale_factor * scaled_aaw; - } - - color = finalize_color(v_color[i / 2], corners[i], unit_normal); - emit_gl_Position(corners[i]); - EmitVertex(); + for (int i = 0; i < MAX_STEPS; i++){ + if (i >= n_steps) break; + float t = subdivision[i]; + emit_point_with_width( + points[i], + tangent_on_curve(t), + joint_products[i], // TODO + mix(v_stroke_width[0], v_stroke_width[2], t), + mix(v_color[0], v_color[2], t), + scaled_aaw + ); } EndPrimitive(); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index ea290bfa..eebf7f53 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -20,8 +20,7 @@ const float STROKE_WIDTH_CONVERSION = 0.01; void main(){ verts = point; - v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width; - v_stroke_width *= mix(frame_scale, 1, is_fixed_in_frame); + v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame); v_joint_product = joint_product; v_color = stroke_rgba; } \ No newline at end of file From c6a6503544a0caaa92d2ca61ea841f3b693157bb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 31 Jul 2024 16:27:37 -0400 Subject: [PATCH 26/59] Cleaning up first pass implementation --- .../shaders/quadratic_bezier_stroke/frag.glsl | 3 +- .../shaders/quadratic_bezier_stroke/geom.glsl | 83 ++++++++++++------- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index a8170ec5..564b062d 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -13,6 +13,5 @@ void main() { // sdf for the region around the curve we wish to color. float signed_dist_to_region = abs(signed_dist_to_curve) - 0.5 * uv_stroke_width; - frag_color.a *= smoothstep(1.0, 0.0, signed_dist_to_region / uv_anti_alias_width); - frag_color.a += 0.2; // undo + frag_color.a *= smoothstep(0.5, -0.5, signed_dist_to_region / uv_anti_alias_width); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 7c4a075d..6e482756 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -34,7 +34,6 @@ const int MAX_STEPS = 16; vec3 unit_normal = vec3(0.0, 0.0, 1.0); #INSERT emit_gl_Position.glsl -#INSERT get_xyz_to_uv.glsl #INSERT finalize_color.glsl @@ -52,6 +51,47 @@ vec4 normalized_joint_product(vec4 joint_product){ } +vec3 point_on_curve(float t){ + return verts[0] + 2 * (verts[1] - verts[0]) * t + (verts[0] - 2 * verts[1] + verts[2]) * t * t; +} + + +vec3 tangent_on_curve(float t){ + return 2 * (verts[1] + -verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t; +} + + +void map_to_basic(out float x0, out float x2, out float scale_factor){ + /* Find the coordinates and scale factor such that the bezier curve + defined by verts[] is congruent to a section of the parabola y = x^2 + between x0 and x2, with scale_factor + */ +} + + +void compute_subdivision(out int n_steps, out float subdivision[MAX_STEPS]){ + /* + Based on https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html + */ + float x0; + float x2; + float scale_factor; + map_to_basic(x0, x2, scale_factor); + + if (normalized_joint_product(v_joint_product[1]).w > COS_THRESHOLD){ + // Linear + n_steps = 2; + }else{ + n_steps = MAX_STEPS; // TODO + } + + for(int i = 0; i < MAX_STEPS; i++){ + if (i >= n_steps) break; + subdivision[i] = float(i) / (n_steps - 1); + } +} + + void create_joint( vec4 joint_product, vec3 unit_tan, @@ -81,7 +121,8 @@ void create_joint( changing_c1 = static_c1 + shift * unit_tan; } -vec3 get_perp(vec4 joint_product, vec3 point, vec3 tangent){ + +vec3 get_perp(vec3 point, vec3 tangent, vec4 joint_product){ /* Perpendicular vectors to the left of the curve */ @@ -101,16 +142,6 @@ vec3 get_perp(vec4 joint_product, vec3 point, vec3 tangent){ } -vec3 point_on_curve(float t){ - return verts[0] + 2 * (verts[1] - verts[0]) * t + (verts[0] - 2 * verts[1] + verts[2]) * t * t; -} - - -vec3 tangent_on_curve(float t){ - return 2 * (verts[1] + -verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t; -} - - void emit_point_with_width( vec3 point, vec3 tangent, @@ -122,7 +153,7 @@ void emit_point_with_width( vec3 unit_tan = normalize(tangent); vec4 njp = normalized_joint_product(joint_product); float buff = 0.5 * width + aaw; - vec3 perp = buff * get_perp(njp, point, unit_tan); + vec3 perp = buff * get_perp(point, unit_tan, njp); vec3 corners[2] = vec3[2](point + perp, point - perp); create_joint( @@ -142,7 +173,6 @@ void emit_point_with_width( emit_gl_Position(corners[i]); EmitVertex(); } - } void main() { @@ -150,27 +180,18 @@ void main() { // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - vec4 jp1 = normalized_joint_product(v_joint_product[1]); - bool is_linear = jp1.w > COS_THRESHOLD; // TODO, something with this - // Compute subdivision int n_steps; - if (is_linear){ - n_steps = 2; - }else{ - n_steps = MAX_STEPS; // TODO - } - float subdivision[MAX_STEPS]; - vec3 points[MAX_STEPS]; - for(int i = 0; i < MAX_STEPS; i++){ - if (i >= n_steps) break; - subdivision[i] = float(i) / (n_steps - 1); - points[i] = point_on_curve(subdivision[i]); - } + compute_subdivision(n_steps, subdivision); // Compute joint products + vec3 points[MAX_STEPS]; vec4 joint_products[MAX_STEPS]; + for (int i = 0; i < MAX_STEPS; i++){ + if (i >= n_steps) break; + points[i] = point_on_curve(subdivision[i]); + } joint_products[0] = v_joint_product[0]; joint_products[0].xyz *= -1; joint_products[n_steps - 1] = v_joint_product[2]; @@ -182,7 +203,7 @@ void main() { joint_products[i].w = dot(v1, v2); } - // Intermediate points + // Emit vertex pairs aroudn subdivided points float scaled_aaw = anti_alias_width * pixel_size; for (int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; @@ -190,7 +211,7 @@ void main() { emit_point_with_width( points[i], tangent_on_curve(t), - joint_products[i], // TODO + joint_products[i], mix(v_stroke_width[0], v_stroke_width[2], t), mix(v_color[0], v_color[2], t), scaled_aaw From a3469c236e10fa72eba97dab5cdf2840aadb8085 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 1 Aug 2024 06:32:04 -0500 Subject: [PATCH 27/59] Simpler compute_subdivisions --- .../shaders/quadratic_bezier_stroke/geom.glsl | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 6e482756..67f07769 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -1,12 +1,13 @@ #version 330 layout (triangles) in; -layout (triangle_strip, max_vertices = 32) out; // Related to MAX_STEPS below +layout (triangle_strip, max_vertices = 64) out; // Related to MAX_STEPS below uniform float anti_alias_width; uniform float flat_stroke; uniform float pixel_size; uniform float joint_type; +uniform float frame_scale; in vec3 verts[3]; @@ -18,6 +19,7 @@ out vec4 color; out float uv_stroke_width; out float uv_anti_alias_width; out float signed_dist_to_curve; +out float flag; // Codes for joint types const int NO_JOINT = 0; @@ -28,8 +30,10 @@ const int MITER_JOINT = 3; // When the cosine of the angle between // two vectors is larger than this, we // consider them aligned -const float COS_THRESHOLD = 0.999; -const int MAX_STEPS = 16; +const float COS_THRESHOLD = 0.99; +// Used to determine how many lines to break the curve into +const float POLYLINE_FACTOR = 20; +const int MAX_STEPS = 32; vec3 unit_normal = vec3(0.0, 0.0, 1.0); @@ -57,34 +61,17 @@ vec3 point_on_curve(float t){ vec3 tangent_on_curve(float t){ - return 2 * (verts[1] + -verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t; -} - - -void map_to_basic(out float x0, out float x2, out float scale_factor){ - /* Find the coordinates and scale factor such that the bezier curve - defined by verts[] is congruent to a section of the parabola y = x^2 - between x0 and x2, with scale_factor - */ + return 2 * (verts[1] - verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t; } void compute_subdivision(out int n_steps, out float subdivision[MAX_STEPS]){ - /* - Based on https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html - */ - float x0; - float x2; - float scale_factor; - map_to_basic(x0, x2, scale_factor); - - if (normalized_joint_product(v_joint_product[1]).w > COS_THRESHOLD){ - // Linear - n_steps = 2; - }else{ - n_steps = MAX_STEPS; // TODO - } + // Crude estimate for the number of polyline segments to use, based + // on the area spanned by the control points + float area = 0.5 * length(v_joint_product[1].xzy); + int count = 2 + int(round(POLYLINE_FACTOR * sqrt(area) / frame_scale)); + n_steps = min(count, MAX_STEPS); for(int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; subdivision[i] = float(i) / (n_steps - 1); @@ -176,6 +163,7 @@ void emit_point_with_width( } void main() { + flag = 0.0; // Curves are marked as ended when the handle after // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; From 361817b5065f461e8e51b373ce72dfe7cc271d1a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 1 Aug 2024 07:17:26 -0500 Subject: [PATCH 28/59] Pass fewer values to frag shader --- .../shaders/quadratic_bezier_stroke/frag.glsl | 15 ++++++----- .../shaders/quadratic_bezier_stroke/geom.glsl | 26 ++++++++++--------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index 564b062d..a3de7d28 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -1,17 +1,20 @@ #version 330 -in float signed_dist_to_curve; -in float uv_stroke_width; -in float uv_anti_alias_width; +// Value between -1 and 1 +in float scaled_signed_dist_to_curve; +in float scaled_anti_alias_width; in vec4 color; out vec4 frag_color; void main() { - if (uv_stroke_width == 0) discard; frag_color = color; // sdf for the region around the curve we wish to color. - float signed_dist_to_region = abs(signed_dist_to_curve) - 0.5 * uv_stroke_width; - frag_color.a *= smoothstep(0.5, -0.5, signed_dist_to_region / uv_anti_alias_width); + float signed_dist_to_region = abs(scaled_signed_dist_to_curve) - 1.0; + frag_color.a *= smoothstep( + scaled_anti_alias_width, + -scaled_anti_alias_width, + signed_dist_to_region + ); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 67f07769..71f52ace 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -16,9 +16,8 @@ in float v_stroke_width[3]; in vec4 v_color[3]; out vec4 color; -out float uv_stroke_width; -out float uv_anti_alias_width; -out float signed_dist_to_curve; +out float scaled_anti_alias_width; +out float scaled_signed_dist_to_curve; out float flag; // Codes for joint types @@ -142,7 +141,7 @@ void emit_point_with_width( float buff = 0.5 * width + aaw; vec3 perp = buff * get_perp(point, unit_tan, njp); - vec3 corners[2] = vec3[2](point + perp, point - perp); + vec3 corners[2] = vec3[2](point + perp, point - perp); create_joint( njp, unit_tan, length(perp), corners[0], corners[0], @@ -150,16 +149,19 @@ void emit_point_with_width( ); color = finalize_color(joint_color, point, unit_normal); - uv_anti_alias_width = aaw; - uv_stroke_width = width; + if (width == 0) scaled_anti_alias_width = 0; + else scaled_anti_alias_width = 2.0 * aaw / width; // Emit two corners - for(int i = 0; i < 2; i++){ - float sign = i % 2 == 0 ? -1 : 1; - signed_dist_to_curve = sign * buff; - emit_gl_Position(corners[i]); - EmitVertex(); - } + // The frag shader will just receive a value from -1 to 1, reflecting where in the + // stroke that point is + scaled_signed_dist_to_curve = -1.0; + emit_gl_Position(corners[0]); + EmitVertex(); + + scaled_signed_dist_to_curve = +1.0; + emit_gl_Position(corners[1]); + EmitVertex(); } void main() { From aea747b6d3de7a4cc269f3e005b8b0b62916b3b8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 1 Aug 2024 07:41:32 -0500 Subject: [PATCH 29/59] Final refinements on polyline stroke implementations --- .../shaders/quadratic_bezier_stroke/frag.glsl | 4 +-- .../shaders/quadratic_bezier_stroke/geom.glsl | 35 ++++++++----------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index a3de7d28..6f6ffc1e 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -8,13 +8,13 @@ in vec4 color; out vec4 frag_color; void main() { + if(scaled_anti_alias_width < 0) discard; frag_color = color; // sdf for the region around the curve we wish to color. float signed_dist_to_region = abs(scaled_signed_dist_to_curve) - 1.0; frag_color.a *= smoothstep( - scaled_anti_alias_width, - -scaled_anti_alias_width, + 0, -scaled_anti_alias_width, signed_dist_to_region ); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 71f52ace..abe67af2 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -18,7 +18,6 @@ in vec4 v_color[3]; out vec4 color; out float scaled_anti_alias_width; out float scaled_signed_dist_to_curve; -out float flag; // Codes for joint types const int NO_JOINT = 0; @@ -31,7 +30,7 @@ const int MITER_JOINT = 3; // consider them aligned const float COS_THRESHOLD = 0.99; // Used to determine how many lines to break the curve into -const float POLYLINE_FACTOR = 20; +const float POLYLINE_FACTOR = 30; const int MAX_STEPS = 32; vec3 unit_normal = vec3(0.0, 0.0, 1.0); @@ -108,7 +107,7 @@ void create_joint( } -vec3 get_perp(vec3 point, vec3 tangent, vec4 joint_product){ +vec3 left_step(vec3 point, vec3 tangent, vec4 joint_product){ /* Perpendicular vectors to the left of the curve */ @@ -133,39 +132,35 @@ void emit_point_with_width( vec3 tangent, vec4 joint_product, float width, - vec4 joint_color, - float aaw + vec4 joint_color ){ vec3 unit_tan = normalize(tangent); - vec4 njp = normalized_joint_product(joint_product); - float buff = 0.5 * width + aaw; - vec3 perp = buff * get_perp(point, unit_tan, njp); + vec4 normed_join_product = normalized_joint_product(joint_product); + vec3 perp = 0.5 * width * left_step(point, unit_tan, normed_join_product); vec3 corners[2] = vec3[2](point + perp, point - perp); create_joint( - njp, unit_tan, length(perp), + normed_join_product, unit_tan, length(perp), corners[0], corners[0], corners[1], corners[1] ); color = finalize_color(joint_color, point, unit_normal); - if (width == 0) scaled_anti_alias_width = 0; - else scaled_anti_alias_width = 2.0 * aaw / width; + if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in frag + else scaled_anti_alias_width = 2.0 * anti_alias_width * pixel_size / width; // Emit two corners - // The frag shader will just receive a value from -1 to 1, reflecting where in the - // stroke that point is + // The frag shader will receive a value from -1 to 1, + // reflecting where in the stroke that point is scaled_signed_dist_to_curve = -1.0; emit_gl_Position(corners[0]); EmitVertex(); - scaled_signed_dist_to_curve = +1.0; emit_gl_Position(corners[1]); EmitVertex(); } void main() { - flag = 0.0; // Curves are marked as ended when the handle after // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; @@ -174,14 +169,14 @@ void main() { int n_steps; float subdivision[MAX_STEPS]; compute_subdivision(n_steps, subdivision); - - // Compute joint products vec3 points[MAX_STEPS]; - vec4 joint_products[MAX_STEPS]; for (int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; points[i] = point_on_curve(subdivision[i]); } + + // Compute joint products + vec4 joint_products[MAX_STEPS]; joint_products[0] = v_joint_product[0]; joint_products[0].xyz *= -1; joint_products[n_steps - 1] = v_joint_product[2]; @@ -194,7 +189,6 @@ void main() { } // Emit vertex pairs aroudn subdivided points - float scaled_aaw = anti_alias_width * pixel_size; for (int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; float t = subdivision[i]; @@ -203,8 +197,7 @@ void main() { tangent_on_curve(t), joint_products[i], mix(v_stroke_width[0], v_stroke_width[2], t), - mix(v_color[0], v_color[2], t), - scaled_aaw + mix(v_color[0], v_color[2], t) ); } EndPrimitive(); From 5decf810e789d5fac13052f9599142415803e46a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 1 Aug 2024 07:41:45 -0500 Subject: [PATCH 30/59] Change default anti_alias_width to 1.5 --- 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 41567f42..f48a016e 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -100,7 +100,7 @@ class VMobject(Mobject): flat_stroke: bool = True, use_simple_quadratic_approx: bool = False, # Measured in pixel widths - anti_alias_width: float = 1.0, + anti_alias_width: float = 1.5, fill_border_width: float = 0.5, use_winding_fill: bool = True, **kwargs From 71814a118bbe5ff4718fc7325156ac8d4046cebd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 1 Aug 2024 07:56:26 -0500 Subject: [PATCH 31/59] Fix dot updater in graph example --- example_scenes.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index 80111134..8ffa857d 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -488,10 +488,7 @@ class GraphExample(Scene): # with the intent of having other mobjects update based # on the parameter x_tracker = ValueTracker(2) - f_always( - dot.move_to, - lambda: axes.i2gp(x_tracker.get_value(), parabola) - ) + dot.add_updater(lambda d: d.move_to(axes.i2gp(x_tracker.get_value(), parabola))) self.play(x_tracker.animate.set_value(4), run_time=3) self.play(x_tracker.animate.set_value(-2), run_time=3) From b3bbc31ea940a46ba2909d7f2b6a819ac0c339a9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 3 Aug 2024 08:12:00 -0500 Subject: [PATCH 32/59] Small clean up to stroke shaders --- .../shaders/quadratic_bezier_stroke/frag.glsl | 5 +- .../shaders/quadratic_bezier_stroke/geom.glsl | 79 ++++++++----------- 2 files changed, 34 insertions(+), 50 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index 6f6ffc1e..db453855 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -13,8 +13,5 @@ void main() { // sdf for the region around the curve we wish to color. float signed_dist_to_region = abs(scaled_signed_dist_to_curve) - 1.0; - frag_color.a *= smoothstep( - 0, -scaled_anti_alias_width, - signed_dist_to_region - ); + frag_color.a *= smoothstep(0, -scaled_anti_alias_width, signed_dist_to_region); } \ No newline at end of file diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index abe67af2..6b76e5d3 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -53,27 +53,13 @@ vec4 normalized_joint_product(vec4 joint_product){ } -vec3 point_on_curve(float t){ - return verts[0] + 2 * (verts[1] - verts[0]) * t + (verts[0] - 2 * verts[1] + verts[2]) * t * t; +vec3 point_on_quadratic(float t, vec3 c0, vec3 c1, vec3 c2){ + return c0 + c1 * t + c2 * t * t; } -vec3 tangent_on_curve(float t){ - return 2 * (verts[1] - verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t; -} - - -void compute_subdivision(out int n_steps, out float subdivision[MAX_STEPS]){ - // Crude estimate for the number of polyline segments to use, based - // on the area spanned by the control points - float area = 0.5 * length(v_joint_product[1].xzy); - int count = 2 + int(round(POLYLINE_FACTOR * sqrt(area) / frame_scale)); - - n_steps = min(count, MAX_STEPS); - for(int i = 0; i < MAX_STEPS; i++){ - if (i >= n_steps) break; - subdivision[i] = float(i) / (n_steps - 1); - } +vec3 tangent_on_quadratic(float t, vec3 c1, vec3 c2){ + return c1 + 2 * c2 * t; } @@ -111,18 +97,13 @@ vec3 left_step(vec3 point, vec3 tangent, vec4 joint_product){ /* Perpendicular vectors to the left of the curve */ - // Add correction for sharp angles to prevent weird bevel effects - float mult = 1.0; - if(joint_product.w < -0.75) mult *= 4 * (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; + unit_normal = normal; // Set global unit normal + if(normal.z < 0) normal *= -1; // Choose the "outward" normal direction if(bool(flat_stroke)){ - return mult * normalize(cross(normal, tangent)); + return normalize(cross(normal, tangent)); }else{ - return mult * normalize(cross(camera_position - point, tangent)); + return normalize(cross(camera_position - point, tangent)); } } @@ -135,15 +116,12 @@ void emit_point_with_width( vec4 joint_color ){ vec3 unit_tan = normalize(tangent); - vec4 normed_join_product = normalized_joint_product(joint_product); - vec3 perp = 0.5 * width * left_step(point, unit_tan, normed_join_product); + vec4 unit_jp = normalized_joint_product(joint_product); + vec3 perp = 0.5 * width * left_step(point, unit_tan, unit_jp); - vec3 corners[2] = vec3[2](point + perp, point - perp); - create_joint( - normed_join_product, unit_tan, length(perp), - corners[0], corners[0], - corners[1], corners[1] - ); + vec3 left = point + perp; + vec3 right = point - perp; + create_joint(unit_jp, unit_tan, length(perp), left, left, right, right); color = finalize_color(joint_color, point, unit_normal); if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in frag @@ -153,26 +131,36 @@ void emit_point_with_width( // The frag shader will receive a value from -1 to 1, // reflecting where in the stroke that point is scaled_signed_dist_to_curve = -1.0; - emit_gl_Position(corners[0]); + emit_gl_Position(left); EmitVertex(); scaled_signed_dist_to_curve = +1.0; - emit_gl_Position(corners[1]); + emit_gl_Position(right); EmitVertex(); } + void main() { // Curves are marked as ended when the handle after // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - // Compute subdivision - int n_steps; - float subdivision[MAX_STEPS]; - compute_subdivision(n_steps, subdivision); + // Coefficients such that the quadratic bezier is c0 + c1 * t + c2 * t^2 + vec3 c0 = verts[0]; + vec3 c1 = 2 * (verts[1] - verts[0]); + vec3 c2 = verts[0] - 2 * verts[1] + verts[2]; + + // Estimate how many line segment the curve should be divided into + // based on the area of the triangle defined by these control points + float area = 0.5 * length(v_joint_product[1].xzy); + int count = int(round(POLYLINE_FACTOR * sqrt(area) / frame_scale)); + int n_steps = min(2 + count, MAX_STEPS); + + // Compute points along the curve vec3 points[MAX_STEPS]; for (int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; - points[i] = point_on_curve(subdivision[i]); + float t = float(i) / (n_steps - 1); + points[i] = point_on_quadratic(t, c0, c1, c2); } // Compute joint products @@ -184,17 +172,16 @@ void main() { if (i >= n_steps - 1) break; vec3 v1 = points[i] - points[i - 1]; vec3 v2 = points[i + 1] - points[i]; - joint_products[i].xyz = cross(v1, v2); - joint_products[i].w = dot(v1, v2); + joint_products[i] = vec4(cross(v1, v2), dot(v1, v2)); } // Emit vertex pairs aroudn subdivided points for (int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; - float t = subdivision[i]; + float t = float(i) / (n_steps - 1); emit_point_with_width( points[i], - tangent_on_curve(t), + tangent_on_quadratic(t, c1, c2), joint_products[i], mix(v_stroke_width[0], v_stroke_width[2], t), mix(v_color[0], v_color[2], t) From 26249c34bb322048b2f9972c2ff93eeee3312b61 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 09:15:06 -0500 Subject: [PATCH 33/59] Have non-flat stroke operate based on projecting tangents --- .../shaders/quadratic_bezier_stroke/geom.glsl | 125 ++++++++++-------- .../shaders/quadratic_bezier_stroke/vert.glsl | 2 +- 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 6b76e5d3..4232b3e9 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -32,8 +32,7 @@ const float COS_THRESHOLD = 0.99; // Used to determine how many lines to break the curve into const float POLYLINE_FACTOR = 30; const int MAX_STEPS = 32; - -vec3 unit_normal = vec3(0.0, 0.0, 1.0); +const float MITER_LIMIT = 3.0; #INSERT emit_gl_Position.glsl #INSERT finalize_color.glsl @@ -63,48 +62,57 @@ vec3 tangent_on_quadratic(float t, vec3 c1, vec3 c2){ } -void create_joint( - vec4 joint_product, - vec3 unit_tan, - float buff, - vec3 static_c0, - out vec3 changing_c0, - vec3 static_c1, - out vec3 changing_c1 -){ - float cos_angle = joint_product.w; - if(abs(cos_angle) > COS_THRESHOLD || int(joint_type) == NO_JOINT){ - // No joint - changing_c0 = static_c0; - changing_c1 = static_c1; - return; - } - - float shift; - float sin_angle = length(joint_product.xyz) * sign(joint_product.z); - if(int(joint_type) == MITER_JOINT){ - shift = buff * (-1.0 - cos_angle) / sin_angle; - }else{ - // For a Bevel joint - shift = buff * (1.0 - cos_angle) / sin_angle; - } - changing_c0 = static_c0 - shift * unit_tan; - changing_c1 = static_c1 + shift * unit_tan; +vec4 get_joint_product(vec3 v1, vec3 v2){ + return vec4(cross(v1, v2), dot(v1, v2)); } -vec3 left_step(vec3 point, vec3 tangent, vec4 joint_product){ - /* - Perpendicular vectors to the left of the curve +vec3 project(vec3 vect, vec3 normal){ + /* Project the vector onto the plane perpendicular to a given unit normal */ + return vect - dot(vect, normal) * normal; +} + +vec3 inverse_joint_product(vec3 vect, vec4 joint_product){ + /* + If joint_product represents vec4(cross(v1, v2), dot(v1, v2)), + then given v1, this function recovers v2 */ - vec3 normal = get_joint_unit_normal(joint_product); - unit_normal = normal; // Set global unit normal - if(normal.z < 0) normal *= -1; // Choose the "outward" normal direction - if(bool(flat_stroke)){ - return normalize(cross(normal, tangent)); - }else{ - return normalize(cross(camera_position - point, tangent)); + float dp = joint_product.w; + if (abs(dp) > COS_THRESHOLD) return vect; + vec3 cp = joint_product.xyz; + vec3 perp = cross(cp, vect); + float a = dp / dot(vect, vect); + float b = length(cp) / length(cross(vect, perp)); + return a * vect + b * perp; +} + + +vec3 step_to_corner(vec3 point, vec3 unit_tan, vec3 unit_normal, vec4 joint_product){ + /* + Step the the left of a curve. + First a perpendicular direction is calculated, then it is adjusted + so as to make a joint. + */ + vec3 step = normalize(cross(unit_normal, unit_tan)); + + // Check if an adjustment is needed + float cos_angle = joint_product.w; + if(abs(cos_angle) > 1 - 1e-5 || int(joint_type) == NO_JOINT){ + return step; } + + // Adjust based on the joint + float sin_angle = length(joint_product.xyz) * sign(joint_product.z); + float shift = (int(joint_type) == MITER_JOINT) ? + (cos_angle + 1.0) / sin_angle : + (cos_angle - 1.0) / sin_angle; + + // return step + shift * unit_tan; + vec3 result = step + shift * unit_tan; + if (length(result) > MITER_LIMIT){ + result = MITER_LIMIT * normalize(result); + } + return result; } @@ -115,14 +123,28 @@ void emit_point_with_width( float width, vec4 joint_color ){ - vec3 unit_tan = normalize(tangent); - vec4 unit_jp = normalized_joint_product(joint_product); - vec3 perp = 0.5 * width * left_step(point, unit_tan, unit_jp); + // Normalize relevant vectors + vec3 unit_tan; + vec4 unit_jp; + vec3 unit_normal; + if(bool(flat_stroke)){ + unit_tan = normalize(tangent); + unit_jp = normalized_joint_product(joint_product); + unit_normal = get_joint_unit_normal(joint_product); + }else{ + unit_normal = normalize(camera_position - point); + unit_tan = normalize(project(tangent, unit_normal)); + vec3 adj_tan = inverse_joint_product(tangent, joint_product); + adj_tan = project(adj_tan, unit_normal); + unit_jp = normalized_joint_product(get_joint_product(unit_tan, adj_tan)); + } + if(unit_normal.z < 0) unit_normal *= -1; // Choose the "outward" normal direction - vec3 left = point + perp; - vec3 right = point - perp; - create_joint(unit_jp, unit_tan, length(perp), left, left, right, right); + // Figure out the step from the point to the corners of the + // triangle strip around the polyline + vec3 step = step_to_corner(point, unit_tan, unit_normal, unit_jp); + // Set styling color = finalize_color(joint_color, point, unit_normal); if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in frag else scaled_anti_alias_width = 2.0 * anti_alias_width * pixel_size / width; @@ -130,12 +152,11 @@ void emit_point_with_width( // Emit two corners // The frag shader will receive a value from -1 to 1, // reflecting where in the stroke that point is - scaled_signed_dist_to_curve = -1.0; - emit_gl_Position(left); - EmitVertex(); - scaled_signed_dist_to_curve = +1.0; - emit_gl_Position(right); - EmitVertex(); + for (int sign = -1; sign <= 1; sign += 2){ + scaled_signed_dist_to_curve = sign; + emit_gl_Position(point + 0.5 * width * sign * step); + EmitVertex(); + } } @@ -172,7 +193,7 @@ void main() { if (i >= n_steps - 1) break; vec3 v1 = points[i] - points[i - 1]; vec3 v2 = points[i + 1] - points[i]; - joint_products[i] = vec4(cross(v1, v2), dot(v1, v2)); + joint_products[i] = get_joint_product(v1, v2); } // Emit vertex pairs aroudn subdivided points diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index eebf7f53..3089c318 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -16,7 +16,7 @@ out vec4 v_joint_product; out float v_stroke_width; out vec4 v_color; -const float STROKE_WIDTH_CONVERSION = 0.01; +const float STROKE_WIDTH_CONVERSION = 0.015; void main(){ verts = point; From c61c18486cc9b49101e5f7a5011384c7cc38444a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 13:37:17 -0500 Subject: [PATCH 34/59] Don't bevel corners on inner joints of quadratic bezier curves --- .../shaders/quadratic_bezier_stroke/geom.glsl | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 4232b3e9..beb4f016 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -67,9 +67,9 @@ vec4 get_joint_product(vec3 v1, vec3 v2){ } -vec3 project(vec3 vect, vec3 normal){ +vec3 project(vec3 vect, vec3 unit_normal){ /* Project the vector onto the plane perpendicular to a given unit normal */ - return vect - dot(vect, normal) * normal; + return vect - dot(vect, unit_normal) * unit_normal; } vec3 inverse_joint_product(vec3 vect, vec4 joint_product){ @@ -87,7 +87,7 @@ vec3 inverse_joint_product(vec3 vect, vec4 joint_product){ } -vec3 step_to_corner(vec3 point, vec3 unit_tan, vec3 unit_normal, vec4 joint_product){ +vec3 step_to_corner(vec3 point, vec3 unit_tan, vec3 unit_normal, vec4 joint_product, bool inner_joint){ /* Step the the left of a curve. First a perpendicular direction is calculated, then it is adjusted @@ -95,9 +95,9 @@ vec3 step_to_corner(vec3 point, vec3 unit_tan, vec3 unit_normal, vec4 joint_prod */ vec3 step = normalize(cross(unit_normal, unit_tan)); - // Check if an adjustment is needed + // Check if an adjustment is needed, float cos_angle = joint_product.w; - if(abs(cos_angle) > 1 - 1e-5 || int(joint_type) == NO_JOINT){ + if(inner_joint || int(joint_type) == NO_JOINT || cos_angle > 1 - 1e-5){ return step; } @@ -107,11 +107,11 @@ vec3 step_to_corner(vec3 point, vec3 unit_tan, vec3 unit_normal, vec4 joint_prod (cos_angle + 1.0) / sin_angle : (cos_angle - 1.0) / sin_angle; - // return step + shift * unit_tan; vec3 result = step + shift * unit_tan; if (length(result) > MITER_LIMIT){ result = MITER_LIMIT * normalize(result); } + return result; } @@ -121,28 +121,31 @@ void emit_point_with_width( vec3 tangent, vec4 joint_product, float width, - vec4 joint_color + vec4 joint_color, + bool inner_joint ){ // Normalize relevant vectors vec3 unit_tan; vec4 unit_jp; vec3 unit_normal; - if(bool(flat_stroke)){ + vec3 to_camera = camera_position - point; + if(flat_stroke == 1.0){ unit_tan = normalize(tangent); unit_jp = normalized_joint_product(joint_product); unit_normal = get_joint_unit_normal(joint_product); }else{ - unit_normal = normalize(camera_position - point); + unit_normal = normalize(to_camera); unit_tan = normalize(project(tangent, unit_normal)); vec3 adj_tan = inverse_joint_product(tangent, joint_product); adj_tan = project(adj_tan, unit_normal); unit_jp = normalized_joint_product(get_joint_product(unit_tan, adj_tan)); } - if(unit_normal.z < 0) unit_normal *= -1; // Choose the "outward" normal direction + // Choose the "outward" normal direction + if(to_camera.z * dot(unit_normal, to_camera) < 0) unit_normal *= -1; // Figure out the step from the point to the corners of the // triangle strip around the polyline - vec3 step = step_to_corner(point, unit_tan, unit_normal, unit_jp); + vec3 step = step_to_corner(point, unit_tan, unit_normal, unit_jp, inner_joint); // Set styling color = finalize_color(joint_color, point, unit_normal); @@ -205,7 +208,8 @@ void main() { tangent_on_quadratic(t, c1, c2), joint_products[i], mix(v_stroke_width[0], v_stroke_width[2], t), - mix(v_color[0], v_color[2], t) + mix(v_color[0], v_color[2], t), + (i > 0 && i < n_steps - 1) // Is this an inner joint ); } EndPrimitive(); From f363eaa2fdc42a3a235a7875349146599012d69e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 13:37:34 -0500 Subject: [PATCH 35/59] Add stand in for a ribboning effect --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index beb4f016..d37ab066 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -147,6 +147,16 @@ void emit_point_with_width( // triangle strip around the polyline vec3 step = step_to_corner(point, unit_tan, unit_normal, unit_jp, inner_joint); + // TODO, this gives a somewhat nice effect that's like a ribbon mostly with its + // broad side to the camera. Currently unused by VMobject + if(flat_stroke == 2.0){ + // Rotate the step towards the unit normal by an amount depending + // on the camera position + float cos_angle = dot(unit_normal, normalize(camera_position)); + float sin_angle = sqrt(max(1 - cos_angle * cos_angle, 0)); + step = cos_angle * step + sin_angle * unit_normal; + } + // Set styling color = finalize_color(joint_color, point, unit_normal); if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in frag From 557819ad032305fb72011edf47a6880c02fe5161 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 14:25:15 -0500 Subject: [PATCH 36/59] Remove pre-computation of curve points and joint products --- .../shaders/quadratic_bezier_stroke/geom.glsl | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index d37ab066..30488bec 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -189,37 +189,32 @@ void main() { int count = int(round(POLYLINE_FACTOR * sqrt(area) / frame_scale)); int n_steps = min(2 + count, MAX_STEPS); - // Compute points along the curve - vec3 points[MAX_STEPS]; - for (int i = 0; i < MAX_STEPS; i++){ - if (i >= n_steps) break; - float t = float(i) / (n_steps - 1); - points[i] = point_on_quadratic(t, c0, c1, c2); - } - - // Compute joint products - vec4 joint_products[MAX_STEPS]; - joint_products[0] = v_joint_product[0]; - joint_products[0].xyz *= -1; - joint_products[n_steps - 1] = v_joint_product[2]; - for (int i = 1; i < MAX_STEPS; i++){ - if (i >= n_steps - 1) break; - vec3 v1 = points[i] - points[i - 1]; - vec3 v2 = points[i + 1] - points[i]; - joint_products[i] = get_joint_product(v1, v2); - } - // Emit vertex pairs aroudn subdivided points for (int i = 0; i < MAX_STEPS; i++){ if (i >= n_steps) break; float t = float(i) / (n_steps - 1); + + // Point and tangent + vec3 point = point_on_quadratic(t, c0, c1, c2); + vec3 tangent = tangent_on_quadratic(t, c1, c2); + + // Style + float stroke_width = mix(v_stroke_width[0], v_stroke_width[2], t); + vec4 color = mix(v_color[0], v_color[2], t); + + // Use middle joint product for inner points, flip cross sign for first + vec4 joint_product; + if (i == 0) joint_product = v_joint_product[0] * vec4(-1, -1, -1, 1); + else if (i < n_steps - 1) joint_product = v_joint_product[1]; + else joint_product = v_joint_product[2]; + + // This is sent along to prevent needless joint creation + bool inside_curve = (i > 0 && i < n_steps - 1); + emit_point_with_width( - points[i], - tangent_on_quadratic(t, c1, c2), - joint_products[i], - mix(v_stroke_width[0], v_stroke_width[2], t), - mix(v_color[0], v_color[2], t), - (i > 0 && i < n_steps - 1) // Is this an inner joint + point, tangent, joint_product, + stroke_width, color, + inside_curve ); } EndPrimitive(); From a5926195eed81ed0b09f94b4d68df9e2968d00f2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 15:01:02 -0500 Subject: [PATCH 37/59] Clean up stroke shader --- .../shaders/quadratic_bezier_stroke/geom.glsl | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 30488bec..4a673fa5 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -87,32 +87,46 @@ vec3 inverse_joint_product(vec3 vect, vec4 joint_product){ } -vec3 step_to_corner(vec3 point, vec3 unit_tan, vec3 unit_normal, vec4 joint_product, bool inner_joint){ +vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_product, bool inner_joint){ /* Step the the left of a curve. First a perpendicular direction is calculated, then it is adjusted so as to make a joint. */ + vec3 unit_tan = normalize(flat_stroke == 0.0 ? project(tangent, unit_normal) : tangent); + vec3 step = normalize(cross(unit_normal, unit_tan)); - // Check if an adjustment is needed, - float cos_angle = joint_product.w; - if(inner_joint || int(joint_type) == NO_JOINT || cos_angle > 1 - 1e-5){ - return step; + // Check if we can avoid creating a joint + if (inner_joint || int(joint_type) == NO_JOINT) return step; + + // Find the appropriate unit joint product + vec4 unit_jp; + if (flat_stroke == 0){ + // Figure out what joint product would be for everything projected onto + // the plane perpendicular to the normal direction (which here would be to_camera) + vec3 adj_tan = inverse_joint_product(tangent, joint_product); + adj_tan = project(adj_tan, unit_normal); + unit_jp = normalized_joint_product(get_joint_product(unit_tan, adj_tan)); + }else { + unit_jp = normalized_joint_product(joint_product); } + float cos_angle = unit_jp.w; + if(cos_angle > 1 - 1e-5) return step; + // Adjust based on the joint - float sin_angle = length(joint_product.xyz) * sign(joint_product.z); + float sin_angle = length(unit_jp.xyz) * sign(unit_jp.z); float shift = (int(joint_type) == MITER_JOINT) ? (cos_angle + 1.0) / sin_angle : (cos_angle - 1.0) / sin_angle; - vec3 result = step + shift * unit_tan; - if (length(result) > MITER_LIMIT){ - result = MITER_LIMIT * normalize(result); + step = step + shift * unit_tan; + if (length(step) > MITER_LIMIT){ + step = MITER_LIMIT * normalize(step); } - return result; + return step; } @@ -125,27 +139,16 @@ void emit_point_with_width( bool inner_joint ){ // Normalize relevant vectors - vec3 unit_tan; - vec4 unit_jp; - vec3 unit_normal; vec3 to_camera = camera_position - point; - if(flat_stroke == 1.0){ - unit_tan = normalize(tangent); - unit_jp = normalized_joint_product(joint_product); - unit_normal = get_joint_unit_normal(joint_product); - }else{ - unit_normal = normalize(to_camera); - unit_tan = normalize(project(tangent, unit_normal)); - vec3 adj_tan = inverse_joint_product(tangent, joint_product); - adj_tan = project(adj_tan, unit_normal); - unit_jp = normalized_joint_product(get_joint_product(unit_tan, adj_tan)); - } + vec3 unit_normal = (flat_stroke == 0.0) ? + normalize(to_camera) : + get_joint_unit_normal(joint_product); // Choose the "outward" normal direction if(to_camera.z * dot(unit_normal, to_camera) < 0) unit_normal *= -1; // Figure out the step from the point to the corners of the // triangle strip around the polyline - vec3 step = step_to_corner(point, unit_tan, unit_normal, unit_jp, inner_joint); + vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inner_joint); // TODO, this gives a somewhat nice effect that's like a ribbon mostly with its // broad side to the camera. Currently unused by VMobject @@ -159,7 +162,7 @@ void emit_point_with_width( // Set styling color = finalize_color(joint_color, point, unit_normal); - if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in frag + if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in the frag shader else scaled_anti_alias_width = 2.0 * anti_alias_width * pixel_size / width; // Emit two corners @@ -202,15 +205,15 @@ void main() { float stroke_width = mix(v_stroke_width[0], v_stroke_width[2], t); vec4 color = mix(v_color[0], v_color[2], t); - // Use middle joint product for inner points, flip cross sign for first - vec4 joint_product; - if (i == 0) joint_product = v_joint_product[0] * vec4(-1, -1, -1, 1); - else if (i < n_steps - 1) joint_product = v_joint_product[1]; - else joint_product = v_joint_product[2]; - // This is sent along to prevent needless joint creation bool inside_curve = (i > 0 && i < n_steps - 1); + // Use middle joint product for inner points, flip sign for first one's cross product component + vec4 joint_product; + if (i == 0) joint_product = v_joint_product[0] * vec4(-1, -1, -1, 1); + else if (inside_curve) joint_product = v_joint_product[1]; + else joint_product = v_joint_product[2]; + emit_point_with_width( point, tangent, joint_product, stroke_width, color, From 2b6ec2d95fe63467db1d37c15be11f9983f2db45 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 15:01:20 -0500 Subject: [PATCH 38/59] Add spacing on assert lines --- manimlib/mobject/types/vectorized_mobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index f48a016e..27479edf 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -673,7 +673,7 @@ class VMobject(Mobject): return bool((dots > 1 - 1e-3).all()) def change_anchor_mode(self, mode: str) -> Self: - assert(mode in ("jagged", "approx_smooth", "true_smooth")) + assert mode in ("jagged", "approx_smooth", "true_smooth") if self.get_num_points() == 0: return self subpaths = self.get_subpaths() @@ -722,7 +722,7 @@ class VMobject(Mobject): return self def add_subpath(self, points: Vect3Array) -> Self: - assert(len(points) % 2 == 1 or len(points) == 0) + assert len(points) % 2 == 1 or len(points) == 0 if not self.has_points(): self.set_points(points) return self From b45c71d3c2cd332c05506b0dc088a917159c98c4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 5 Aug 2024 16:58:03 -0500 Subject: [PATCH 39/59] Account for Gimbal lock in panning --- manimlib/camera/camera_frame.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py index 9c9c31ce..40dd06eb 100644 --- a/manimlib/camera/camera_frame.py +++ b/manimlib/camera/camera_frame.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +import warnings import numpy as np from scipy.spatial.transform import Rotation @@ -9,8 +10,10 @@ from pyrr import Matrix44 from manimlib.constants import DEGREES, RADIANS from manimlib.constants import FRAME_SHAPE from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP +from manimlib.constants import PI from manimlib.mobject.mobject import Mobject from manimlib.utils.space_ops import normalize +from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING @@ -62,9 +65,16 @@ class CameraFrame(Mobject): def get_euler_angles(self) -> np.ndarray: orientation = self.get_orientation() - if all(orientation.as_quat() == [0, 0, 0, 1]): + if np.isclose(orientation.as_quat(), [0, 0, 0, 1]).all(): return np.zeros(3) - return orientation.as_euler(self.euler_axes)[::-1] + with warnings.catch_warnings(): + warnings.simplefilter('ignore', UserWarning) # Ignore UserWarnings + angles = orientation.as_euler(self.euler_axes)[::-1] + # Handle Gimble lock case + if np.isclose(angles[1], 0, atol=1e-2): + angles[0] = angles[0] + angles[2] + angles[2] = 0 + return angles def get_theta(self): return self.get_euler_angles()[0] @@ -134,16 +144,16 @@ class CameraFrame(Mobject): def increment_euler_angles( self, - dtheta: float | None = None, - dphi: float | None = None, - dgamma: float | None = None, + dtheta: float = 0, + dphi: float = 0, + dgamma: float = 0, units: float = RADIANS ): angles = self.get_euler_angles() - for i, value in enumerate([dtheta, dphi, dgamma]): - if value is not None: - angles[i] += value * units - self.set_euler_angles(*angles) + new_angles = angles + np.array([dtheta, dphi, dgamma]) * units + new_angles[1] = clip(new_angles[1], 0, PI) # Limit range for phi + new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1]) + self.set_orientation(new_rot) return self def set_euler_axes(self, seq: str): From 4223bb63202e5e122e0bc95c842ae5829fe6d62d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 6 Aug 2024 10:23:46 -0500 Subject: [PATCH 40/59] Small cleanup on TracedPath --- manimlib/mobject/changing.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/manimlib/mobject/changing.py b/manimlib/mobject/changing.py index e348b7c1..01a73560 100644 --- a/manimlib/mobject/changing.py +++ b/manimlib/mobject/changing.py @@ -103,21 +103,16 @@ class TracedPath(VMobject): time_per_anchor: float = 1.0 / 15, stroke_width: float | Iterable[float] = 2.0, stroke_color: ManimColor = WHITE, - fill_opacity: float = 0.0, **kwargs ): - super().__init__( - stroke_width=stroke_width, - stroke_color=stroke_color, - fill_opacity=fill_opacity, - **kwargs - ) + super().__init__(**kwargs) self.traced_point_func = traced_point_func self.time_traced = time_traced self.time_per_anchor = time_per_anchor self.time: float = 0 self.traced_points: list[np.ndarray] = [] self.add_updater(lambda m, dt: m.update_path(dt)) + self.set_stroke(stroke_color, stroke_width) def update_path(self, dt: float) -> Self: if dt == 0: From 4ff61ed561fed31d324dcf8613a964c7a2f7f5e0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 6 Aug 2024 10:27:35 -0500 Subject: [PATCH 41/59] Default approximate smoothing (non-approx has a bug in 3d) --- 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 27479edf..34ed8afc 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -697,7 +697,7 @@ class VMobject(Mobject): self.add_subpath(new_subpath) return self - def make_smooth(self, approx=False, recurse=True) -> Self: + def make_smooth(self, approx=True, recurse=True) -> Self: """ Edits the path so as to pass smoothly through all the current anchor points. From 44ec9933b7396ee20546916237622fca5211a9fb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 6 Aug 2024 10:39:23 -0500 Subject: [PATCH 42/59] Small format fix --- 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 34ed8afc..e28d3cdc 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1201,7 +1201,7 @@ class VMobject(Mobject): points = self.get_points() - if(len(points) < 3): + if len(points) < 3: return self.data["joint_product"] # Find all the unit tangent vectors at each joint From ec88673e927af5292925ce40f18f820c12bbe9df Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 6 Aug 2024 10:41:31 -0500 Subject: [PATCH 43/59] Fix kink issue in flat stroke --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 4a673fa5..bfe4b5e2 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -116,7 +116,7 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_produ if(cos_angle > 1 - 1e-5) return step; // Adjust based on the joint - float sin_angle = length(unit_jp.xyz) * sign(unit_jp.z); + float sin_angle = length(unit_jp.xyz) * sign(dot(unit_jp.xyz, unit_normal)); float shift = (int(joint_type) == MITER_JOINT) ? (cos_angle + 1.0) / sin_angle : (cos_angle - 1.0) / sin_angle; @@ -144,7 +144,7 @@ void emit_point_with_width( normalize(to_camera) : get_joint_unit_normal(joint_product); // Choose the "outward" normal direction - if(to_camera.z * dot(unit_normal, to_camera) < 0) unit_normal *= -1; + unit_normal *= sign(dot(unit_normal, to_camera)); // Figure out the step from the point to the corners of the // triangle strip around the polyline From edb438e5e23a670e16cc5890762f5ea7aedb5be0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 6 Aug 2024 15:45:21 -0500 Subject: [PATCH 44/59] Further clean up to stroke shader --- .../shaders/quadratic_bezier_stroke/geom.glsl | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index bfe4b5e2..51f7a412 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -32,23 +32,21 @@ const float COS_THRESHOLD = 0.99; // Used to determine how many lines to break the curve into const float POLYLINE_FACTOR = 30; const int MAX_STEPS = 32; -const float MITER_LIMIT = 3.0; +const float MITER_LIMIT = 5.0; #INSERT emit_gl_Position.glsl #INSERT finalize_color.glsl vec3 get_joint_unit_normal(vec4 joint_product){ - vec3 result = (joint_product.w < COS_THRESHOLD) ? - joint_product.xyz : v_joint_product[1].xyz; - float norm = length(result); - return (norm > 1e-5) ? result / norm : vec3(0.0, 0.0, 1.0); -} - - -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); + float tol = 1e-8; + if (length(joint_product.xyz) > tol){ + return normalize(joint_product.xyz); + } + if (length(v_joint_product[1].xyz) > tol){ + return normalize(v_joint_product[1].xyz); + } + return vec3(0.0, 0.0, 1.0); } @@ -72,18 +70,12 @@ vec3 project(vec3 vect, vec3 unit_normal){ return vect - dot(vect, unit_normal) * unit_normal; } -vec3 inverse_joint_product(vec3 vect, vec4 joint_product){ +vec3 inverse_vector_product(vec3 vect, vec3 cross_product, float dot_product){ /* - If joint_product represents vec4(cross(v1, v2), dot(v1, v2)), - then given v1, this function recovers v2 + Suppose cross(v1, v2) = cross_product and dot(v1, v2) = dot_product. + Given v1, this function return v2. */ - float dp = joint_product.w; - if (abs(dp) > COS_THRESHOLD) return vect; - vec3 cp = joint_product.xyz; - vec3 perp = cross(cp, vect); - float a = dp / dot(vect, vect); - float b = length(cp) / length(cross(vect, perp)); - return a * vect + b * perp; + return (vect * dot_product - cross(vect, cross_product)) / dot(vect, vect); } @@ -94,29 +86,26 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_produ so as to make a joint. */ vec3 unit_tan = normalize(flat_stroke == 0.0 ? project(tangent, unit_normal) : tangent); - vec3 step = normalize(cross(unit_normal, unit_tan)); // Check if we can avoid creating a joint if (inner_joint || int(joint_type) == NO_JOINT) return step; - // Find the appropriate unit joint product - vec4 unit_jp; + // Find the angle between + if (flat_stroke == 0){ // Figure out what joint product would be for everything projected onto // the plane perpendicular to the normal direction (which here would be to_camera) - vec3 adj_tan = inverse_joint_product(tangent, joint_product); + vec3 adj_tan = inverse_vector_product(tangent, joint_product.xyz, joint_product.w); adj_tan = project(adj_tan, unit_normal); - unit_jp = normalized_joint_product(get_joint_product(unit_tan, adj_tan)); - }else { - unit_jp = normalized_joint_product(joint_product); + joint_product = get_joint_product(unit_tan, adj_tan); } - float cos_angle = unit_jp.w; - if(cos_angle > 1 - 1e-5) return step; + float cos_angle = (length(joint_product) > 1e-10) ? normalize(joint_product).w : 1.0; + if(cos_angle > 1 - 1e-3) return step; + float sin_angle = sqrt(1 - cos_angle * cos_angle) * sign(dot(joint_product.xyz, unit_normal)); // Adjust based on the joint - float sin_angle = length(unit_jp.xyz) * sign(dot(unit_jp.xyz, unit_normal)); float shift = (int(joint_type) == MITER_JOINT) ? (cos_angle + 1.0) / sin_angle : (cos_angle - 1.0) / sin_angle; @@ -138,20 +127,22 @@ void emit_point_with_width( vec4 joint_color, bool inner_joint ){ - // Normalize relevant vectors + // Find unit normal vec3 to_camera = camera_position - point; - vec3 unit_normal = (flat_stroke == 0.0) ? - normalize(to_camera) : - get_joint_unit_normal(joint_product); - // Choose the "outward" normal direction - unit_normal *= sign(dot(unit_normal, to_camera)); + vec3 unit_normal; + if (flat_stroke == 0.0){ + unit_normal = normalize(to_camera); + }else{ + unit_normal = get_joint_unit_normal(joint_product); + unit_normal *= sign(dot(unit_normal, to_camera)); // Choose the "outward" normal direction + } // Figure out the step from the point to the corners of the // triangle strip around the polyline vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inner_joint); - // TODO, this gives a somewhat nice effect that's like a ribbon mostly with its - // broad side to the camera. Currently unused by VMobject + // TODO, this gives a potentially nice effect that's like a ribbon mostly with its + // broad side to the camera. Currently hard to access via VMobject if(flat_stroke == 2.0){ // Rotate the step towards the unit normal by an amount depending // on the camera position From 61a2b4d0da0fdf27d0a57a3fa9107405cb017300 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 10:34:47 -0500 Subject: [PATCH 45/59] Improve flat stroke for sharp corners with a smooth transition to miter joints --- .../shaders/quadratic_bezier_stroke/geom.glsl | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 51f7a412..f2b32e79 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -28,11 +28,10 @@ const int MITER_JOINT = 3; // When the cosine of the angle between // two vectors is larger than this, we // consider them aligned -const float COS_THRESHOLD = 0.99; +const float COS_THRESHOLD = 0.999; // Used to determine how many lines to break the curve into const float POLYLINE_FACTOR = 30; const int MAX_STEPS = 32; -const float MITER_LIMIT = 5.0; #INSERT emit_gl_Position.glsl #INSERT finalize_color.glsl @@ -50,6 +49,13 @@ vec3 get_joint_unit_normal(vec4 joint_product){ } +vec4 unit_joint_product(vec4 joint_product){ + float tol = 1e-8; + float norm = length(joint_product); + return (norm < tol) ? vec4(0.0, 0.0, 0.0, 1.0) : joint_product / norm; +} + + vec3 point_on_quadratic(float t, vec3 c0, vec3 c1, vec3 c2){ return c0 + c1 * t + c2 * t * t; } @@ -86,36 +92,34 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_produ so as to make a joint. */ vec3 unit_tan = normalize(flat_stroke == 0.0 ? project(tangent, unit_normal) : tangent); + vec4 unit_jp = unit_joint_product(joint_product); + float cos_angle = unit_jp.w; + + // Step to stroke width bound should be perpendicular + // both to the tangent and the normal direction vec3 step = normalize(cross(unit_normal, unit_tan)); - // Check if we can avoid creating a joint - if (inner_joint || int(joint_type) == NO_JOINT) return step; + // Conditions where no joint needs to be created + if (inner_joint || int(joint_type) == NO_JOINT || cos_angle > COS_THRESHOLD) return step; - // Find the angle between - if (flat_stroke == 0){ // Figure out what joint product would be for everything projected onto // the plane perpendicular to the normal direction (which here would be to_camera) - vec3 adj_tan = inverse_vector_product(tangent, joint_product.xyz, joint_product.w); + vec3 adj_tan = inverse_vector_product(tangent, unit_jp.xyz, unit_jp.w); adj_tan = project(adj_tan, unit_normal); - joint_product = get_joint_product(unit_tan, adj_tan); + vec4 flat_jp = get_joint_product(unit_tan, adj_tan); + cos_angle = unit_joint_product(flat_jp).w; } - float cos_angle = (length(joint_product) > 1e-10) ? normalize(joint_product).w : 1.0; - if(cos_angle > 1 - 1e-3) return step; + // Adjust based on the joint. + // Bevel for cos(angle) > -0.7, smoothly transition + // to miter for those with sharper angles float sin_angle = sqrt(1 - cos_angle * cos_angle) * sign(dot(joint_product.xyz, unit_normal)); + float miter_factor = (int(joint_type) == MITER_JOINT) ? + 1.0 : smoothstep(-0.7, -0.9, cos_angle); + float shift = (cos_angle + mix(-1, 1, miter_factor)) / sin_angle; - // Adjust based on the joint - float shift = (int(joint_type) == MITER_JOINT) ? - (cos_angle + 1.0) / sin_angle : - (cos_angle - 1.0) / sin_angle; - - step = step + shift * unit_tan; - if (length(step) > MITER_LIMIT){ - step = MITER_LIMIT * normalize(step); - } - - return step; + return step + shift * unit_tan; } @@ -155,6 +159,7 @@ void emit_point_with_width( color = finalize_color(joint_color, point, unit_normal); if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in the frag shader else scaled_anti_alias_width = 2.0 * anti_alias_width * pixel_size / width; + width += anti_alias_width * pixel_size; // Emit two corners // The frag shader will receive a value from -1 to 1, From 0dcf6302226cbd76fa10769e654fd816a9d34ada Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 11:23:37 -0500 Subject: [PATCH 46/59] Change default to non-flat stroke rendering --- 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 e28d3cdc..fe94e78c 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -97,7 +97,7 @@ class VMobject(Mobject): long_lines: bool = False, # Could also be "no_joint", "bevel", "miter" joint_type: str = "auto", - flat_stroke: bool = True, + flat_stroke: bool = False, use_simple_quadratic_approx: bool = False, # Measured in pixel widths anti_alias_width: float = 1.5, From e130625b9b61dcedb8b12b112a306f14e05db22b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 12:12:29 -0500 Subject: [PATCH 47/59] Handle edge case of single point passed into approx_smooth_quadratic_bezier_handles --- manimlib/utils/bezier.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index accf0012..779210a8 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -198,7 +198,9 @@ def approx_smooth_quadratic_bezier_handles( another that would produce a parabola passing through P0, call it smooth_to_left, and use the midpoint between the two. """ - if len(points) == 2: + if len(points) == 1: + return points[0] + elif len(points) == 2: return midpoint(*points) smooth_to_right, smooth_to_left = [ 0.25 * ps[0:-2] + ps[1:-1] - 0.25 * ps[2:] From 0a43a3ff9acda0ab8d9220f8b214233d606b4f37 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 14:10:50 -0500 Subject: [PATCH 48/59] Remove unnecessary stroke vert input --- manimlib/shaders/quadratic_bezier_stroke/vert.glsl | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index 3089c318..6bd488d5 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -6,7 +6,6 @@ uniform float is_fixed_in_frame; in vec3 point; in vec4 stroke_rgba; in float stroke_width; -in vec3 joint_normal; in vec4 joint_product; // Bezier control point From bc91e91634ca10ffae8458e2935ffa9f23b96ade Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 14:11:10 -0500 Subject: [PATCH 49/59] In get_euler_angles, add edge case for gimbal lock on the low side --- manimlib/camera/camera_frame.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py index 40dd06eb..78a7d095 100644 --- a/manimlib/camera/camera_frame.py +++ b/manimlib/camera/camera_frame.py @@ -74,6 +74,9 @@ class CameraFrame(Mobject): if np.isclose(angles[1], 0, atol=1e-2): angles[0] = angles[0] + angles[2] angles[2] = 0 + if np.isclose(angles[1], PI, atol=1e-2): + angles[0] = angles[0] - angles[2] + angles[2] = 0 return angles def get_theta(self): From f677a02036b19f7a90d34ce151ebd147345516cd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 14:45:55 -0500 Subject: [PATCH 50/59] Allow for manually setting miter or bevel joints --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index f2b32e79..d6392d0d 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -112,11 +112,14 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_produ } // Adjust based on the joint. - // Bevel for cos(angle) > -0.7, smoothly transition - // to miter for those with sharper angles + // If joint type is auto, it will bevel for cos(angle) > -0.7, + // and smoothly transition to miter for those with sharper angles + float miter_factor; + if (joint_type == AUTO_JOINT) miter_factor = smoothstep(-0.7, -0.9, cos_angle); + else if (joint_type == BEVEL_JOINT) miter_factor = 0.0; + else miter_factor = 1.0; + float sin_angle = sqrt(1 - cos_angle * cos_angle) * sign(dot(joint_product.xyz, unit_normal)); - float miter_factor = (int(joint_type) == MITER_JOINT) ? - 1.0 : smoothstep(-0.7, -0.9, cos_angle); float shift = (cos_angle + mix(-1, 1, miter_factor)) / sin_angle; return step + shift * unit_tan; From 70862a068f75f3846f2f35d5cbc3add394184435 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 14:46:17 -0500 Subject: [PATCH 51/59] Don't buff out stroke width by antialias width --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index d6392d0d..1b6ecdc9 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -162,7 +162,6 @@ void emit_point_with_width( color = finalize_color(joint_color, point, unit_normal); if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in the frag shader else scaled_anti_alias_width = 2.0 * anti_alias_width * pixel_size / width; - width += anti_alias_width * pixel_size; // Emit two corners // The frag shader will receive a value from -1 to 1, From 099aaaee43f8b44f9f62a9fe42e30cad08e5b5d9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 14:46:26 -0500 Subject: [PATCH 52/59] Increase polyline factor --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 1b6ecdc9..9e9117b6 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -30,7 +30,7 @@ const int MITER_JOINT = 3; // consider them aligned const float COS_THRESHOLD = 0.999; // Used to determine how many lines to break the curve into -const float POLYLINE_FACTOR = 30; +const float POLYLINE_FACTOR = 100; const int MAX_STEPS = 32; #INSERT emit_gl_Position.glsl From bf43a648a436849d5f10256cb0dfb465922a8c98 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 14:54:37 -0500 Subject: [PATCH 53/59] Allow for setting flat stroke in VMobject.set_stroke --- manimlib/mobject/types/vectorized_mobject.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index fe94e78c..753e6399 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -203,6 +203,7 @@ class VMobject(Mobject): width: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None, background: bool | None = None, + flat: bool | None = None, recurse: bool = True ) -> Self: self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse) @@ -221,6 +222,9 @@ class VMobject(Mobject): for mob in self.get_family(recurse): mob.stroke_behind = background + if flat is not None: + self.set_flat_stroke(flat) + self.note_changed_stroke() return self From 1ff758dea878af07c7d60e8b3b56a6c8663137c7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 7 Aug 2024 15:06:10 -0500 Subject: [PATCH 54/59] Remove (no longer necessary) specifications of non-flat stroke for 3d things --- manimlib/mobject/coordinate_systems.py | 3 --- manimlib/mobject/three_dimensions.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 0502b19e..eb0c0b83 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -530,7 +530,6 @@ class ThreeDAxes(Axes): z_axis_config: dict = dict(), z_normal: Vect3 = DOWN, depth: float | None = None, - flat_stroke: bool = False, **kwargs ): Axes.__init__(self, x_range, y_range, **kwargs) @@ -555,8 +554,6 @@ class ThreeDAxes(Axes): self.axes.add(self.z_axis) self.add(self.z_axis) - self.set_flat_stroke(flat_stroke) - def get_all_ranges(self) -> list[Sequence[float]]: return [self.x_range, self.y_range, self.z_range] diff --git a/manimlib/mobject/three_dimensions.py b/manimlib/mobject/three_dimensions.py index 05a12ede..d29b7ec2 100644 --- a/manimlib/mobject/three_dimensions.py +++ b/manimlib/mobject/three_dimensions.py @@ -38,7 +38,6 @@ class SurfaceMesh(VGroup): normal_nudge: float = 1e-2, depth_test: bool = True, joint_type: str = 'no_joint', - flat_stroke: bool = False, **kwargs ): self.uv_surface = uv_surface @@ -52,7 +51,6 @@ class SurfaceMesh(VGroup): joint_type=joint_type, **kwargs ) - self.set_flat_stroke(flat_stroke) def init_points(self) -> None: uv_surface = self.uv_surface From c345d76de09e32e3ff3a7b125aaf77996d6accd3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 8 Aug 2024 11:36:28 -0500 Subject: [PATCH 55/59] Patch for glitches associated with non-flat stroke when tangency direction lines up with camera view --- .../shaders/quadratic_bezier_stroke/geom.glsl | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 9e9117b6..f9c093ec 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -85,33 +85,47 @@ vec3 inverse_vector_product(vec3 vect, vec3 cross_product, float dot_product){ } -vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_product, bool inner_joint){ +vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_product, bool inside_curve){ /* Step the the left of a curve. First a perpendicular direction is calculated, then it is adjusted so as to make a joint. */ vec3 unit_tan = normalize(flat_stroke == 0.0 ? project(tangent, unit_normal) : tangent); - vec4 unit_jp = unit_joint_product(joint_product); - float cos_angle = unit_jp.w; // Step to stroke width bound should be perpendicular // both to the tangent and the normal direction vec3 step = normalize(cross(unit_normal, unit_tan)); - // Conditions where no joint needs to be created - if (inner_joint || int(joint_type) == NO_JOINT || cos_angle > COS_THRESHOLD) return step; + // For non-flat stroke, there can be glitches when the tangent direction + // lines up very closely with the direction to the camera, treated here + // as the unit normal. To avoid those, this smoothly transitions to a step + // direction perpendicular to the true curve normal. + float alignment = abs(dot(normalize(tangent), unit_normal)); + float alignment_threshold = 0.97; // This could maybe be chosen in a more principled way based on stroke width + if (alignment > alignment_threshold) { + vec3 perp = normalize(cross(get_joint_unit_normal(joint_product), tangent)); + step = mix(step, project(step, perp), smoothstep(alignment_threshold, 1.0, alignment)); + } + if (inside_curve || int(joint_type) == NO_JOINT) return step; + + vec4 unit_jp = unit_joint_product(joint_product); + float cos_angle = unit_jp.w; + + if (cos_angle > COS_THRESHOLD) return step; + + // Below here, figure out the adjustment to bevel or miter a joint if (flat_stroke == 0){ // Figure out what joint product would be for everything projected onto // the plane perpendicular to the normal direction (which here would be to_camera) + step = normalize(cross(unit_normal, unit_tan)); // Back to original step vec3 adj_tan = inverse_vector_product(tangent, unit_jp.xyz, unit_jp.w); adj_tan = project(adj_tan, unit_normal); vec4 flat_jp = get_joint_product(unit_tan, adj_tan); cos_angle = unit_joint_product(flat_jp).w; } - // Adjust based on the joint. // If joint type is auto, it will bevel for cos(angle) > -0.7, // and smoothly transition to miter for those with sharper angles float miter_factor; @@ -132,7 +146,7 @@ void emit_point_with_width( vec4 joint_product, float width, vec4 joint_color, - bool inner_joint + bool inside_curve ){ // Find unit normal vec3 to_camera = camera_position - point; @@ -146,7 +160,7 @@ void emit_point_with_width( // Figure out the step from the point to the corners of the // triangle strip around the polyline - vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inner_joint); + vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inside_curve); // TODO, this gives a potentially nice effect that's like a ribbon mostly with its // broad side to the camera. Currently hard to access via VMobject From 9f54b85c4e35a09afa58ac915dcb78f305421e4f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 8 Aug 2024 13:55:25 -0700 Subject: [PATCH 56/59] Change miter threshold to global constant --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index f9c093ec..8db86d75 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -32,6 +32,7 @@ const float COS_THRESHOLD = 0.999; // Used to determine how many lines to break the curve into const float POLYLINE_FACTOR = 100; const int MAX_STEPS = 32; +const float MITER_COS_ANGLE_THRESHOLD = -0.8; #INSERT emit_gl_Position.glsl #INSERT finalize_color.glsl @@ -126,10 +127,12 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_produ cos_angle = unit_joint_product(flat_jp).w; } - // If joint type is auto, it will bevel for cos(angle) > -0.7, + // If joint type is auto, it will bevel for cos(angle) > MITER_COS_ANGLE_THRESHOLD, // and smoothly transition to miter for those with sharper angles + float mcat1 = MITER_COS_ANGLE_THRESHOLD; + float mcat2 = 0.5 * (mcat1 - 1.0); float miter_factor; - if (joint_type == AUTO_JOINT) miter_factor = smoothstep(-0.7, -0.9, cos_angle); + if (joint_type == AUTO_JOINT) miter_factor = smoothstep(mcat1, mcat2, cos_angle); else if (joint_type == BEVEL_JOINT) miter_factor = 0.0; else miter_factor = 1.0; From cff3bdf8d4f10f12bbc7cf45eed7c98979706fd7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 8 Aug 2024 13:55:48 -0700 Subject: [PATCH 57/59] Remove unnecessary flat stroke option --- .../shaders/quadratic_bezier_stroke/geom.glsl | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 8db86d75..91bad287 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -165,21 +165,6 @@ void emit_point_with_width( // triangle strip around the polyline vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inside_curve); - // TODO, this gives a potentially nice effect that's like a ribbon mostly with its - // broad side to the camera. Currently hard to access via VMobject - if(flat_stroke == 2.0){ - // Rotate the step towards the unit normal by an amount depending - // on the camera position - float cos_angle = dot(unit_normal, normalize(camera_position)); - float sin_angle = sqrt(max(1 - cos_angle * cos_angle, 0)); - step = cos_angle * step + sin_angle * unit_normal; - } - - // Set styling - color = finalize_color(joint_color, point, unit_normal); - if (width == 0) scaled_anti_alias_width = -1.0; // Signal to discard in the frag shader - else scaled_anti_alias_width = 2.0 * anti_alias_width * pixel_size / width; - // Emit two corners // The frag shader will receive a value from -1 to 1, // reflecting where in the stroke that point is From 31b6affabb6ae3b8d7fc9c0300adf697b731fc71 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 8 Aug 2024 13:56:16 -0700 Subject: [PATCH 58/59] Push up pointwise styling update --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 91bad287..f7b136db 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -161,6 +161,12 @@ void emit_point_with_width( unit_normal *= sign(dot(unit_normal, to_camera)); // Choose the "outward" normal direction } + // Set styling + color = finalize_color(joint_color, point, unit_normal); + scaled_anti_alias_width = (width == 0) ? + -1.0 : // Signal to discard in the frag shader + 2.0 * anti_alias_width * pixel_size / width; + // Figure out the step from the point to the corners of the // triangle strip around the polyline vec3 step = step_to_corner(point, tangent, unit_normal, joint_product, inside_curve); From a4858918ddd58069021c5c608fd39ef51344149a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 8 Aug 2024 13:59:50 -0700 Subject: [PATCH 59/59] Small reformatting --- manimlib/shaders/quadratic_bezier_stroke/geom.glsl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index f7b136db..fbaeb901 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -129,12 +129,16 @@ vec3 step_to_corner(vec3 point, vec3 tangent, vec3 unit_normal, vec4 joint_produ // If joint type is auto, it will bevel for cos(angle) > MITER_COS_ANGLE_THRESHOLD, // and smoothly transition to miter for those with sharper angles - float mcat1 = MITER_COS_ANGLE_THRESHOLD; - float mcat2 = 0.5 * (mcat1 - 1.0); float miter_factor; - if (joint_type == AUTO_JOINT) miter_factor = smoothstep(mcat1, mcat2, cos_angle); - else if (joint_type == BEVEL_JOINT) miter_factor = 0.0; - else miter_factor = 1.0; + if (joint_type == BEVEL_JOINT){ + miter_factor = 0.0; + }else if (joint_type == MITER_JOINT){ + miter_factor = 1.0; + }else { + float mcat1 = MITER_COS_ANGLE_THRESHOLD; + float mcat2 = 0.5 * (mcat1 - 1.0); + miter_factor = smoothstep(mcat1, mcat2, cos_angle); + } float sin_angle = sqrt(1 - cos_angle * cos_angle) * sign(dot(joint_product.xyz, unit_normal)); float shift = (cos_angle + mix(-1, 1, miter_factor)) / sin_angle;