diff --git a/example_scenes.py b/example_scenes.py index bf817fed..3134d484 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -26,12 +26,12 @@ 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, include_background_rectangle=True, h_buff=1.0), Text("looks like") ) linear_transform_words.arrange(RIGHT) linear_transform_words.to_edge(UP) - linear_transform_words.set_stroke(BLACK, 10, background=True) + linear_transform_words.set_backstroke(width=5) self.play( ShowCreation(grid), @@ -52,7 +52,7 @@ class OpeningManimExample(Scene): this is the map $z \\rightarrow z^2$ """) complex_map_words.to_corner(UR) - complex_map_words.set_stroke(BLACK, 5, background=True) + complex_map_words.set_backstroke(width=5) self.play( FadeOut(grid), @@ -268,16 +268,8 @@ class UpdatersExample(Scene): # that of the newly constructed object brace = always_redraw(Brace, square, UP) - text, number = label = VGroup( - Text("Width = "), - DecimalNumber( - 0, - show_ellipsis=True, - num_decimal_places=2, - include_sign=True, - ) - ) - label.arrange(RIGHT) + label = TexText("Width = 0.00") + number = label.make_number_changable("0.00") # This ensures that the method deicmal.next_to(square) # is called on every frame diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index 66716df8..ac73891d 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -98,11 +98,7 @@ class DrawBorderThenFill(Animation): self.mobject = vmobject def begin(self) -> None: - # Trigger triangulation calculation - for submob in self.mobject.get_family(): - if not submob._use_winding_fill: - submob.get_triangulation() - + self.mobject.set_animating_status(True) self.outline = self.get_outline() super().begin() self.mobject.match_style(self.outline) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 012bfad9..58c4886e 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -70,7 +70,7 @@ class Transform(Animation): def finish(self) -> None: super().finish() self.mobject.unlock_data() - if self.target_mobject is not None: + if self.target_mobject is not None and self.rate_func(1) == 1: self.mobject.become(self.target_mobject) def create_target(self) -> Mobject: diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index 6f44c16d..b832cf0e 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -131,9 +131,10 @@ class TransformMatchingStrings(TransformMatchingParts): target: StringMobject, matched_keys: Iterable[str] = [], key_map: dict[str, str] = dict(), + matched_pairs: Iterable[tuple[Mobject, Mobject]] = [], **kwargs, ): - matched_pairs = [ + matched_pairs = list(matched_pairs) + [ *[(source[key], target[key]) for key in matched_keys], *[(source[key1], target[key2]) for key1, key2 in key_map.items()], *[ diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 557ea618..08fe1abc 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -133,20 +133,11 @@ class Camera(object): gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo) gl.glBlitFramebuffer( *src_fbo.viewport, - *src_fbo.viewport, + *dst_fbo.viewport, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR ) def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes: - # # Copy blocks from fbo into draw_fbo using Blit - # gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo) - # gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo) - # src_viewport = self.fbo.viewport - # gl.glBlitFramebuffer( - # *src_viewport, - # *self.draw_fbo.viewport, - # gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR - # ) self.blit(self.fbo, self.draw_fbo) return self.draw_fbo.read( viewport=self.draw_fbo.viewport, diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py index 9c01af73..deafefe3 100644 --- a/manimlib/camera/camera_frame.py +++ b/manimlib/camera/camera_frame.py @@ -29,15 +29,22 @@ class CameraFrame(Mobject): ): super().__init__(**kwargs) - self.view_matrix = np.identity(4) + self.uniforms["orientation"] = Rotation.identity().as_quat() + self.uniforms["fovy"] = fovy + self.default_orientation = Rotation.identity() + self.view_matrix = np.identity(4) + self.camera_location = OUT # This will be updated by set_points self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP])) self.set_width(frame_shape[0], stretch=True) self.set_height(frame_shape[1], stretch=True) self.move_to(center_point) - self.uniforms["orientation"] = Rotation.identity().as_quat() - self.uniforms["fovy"] = fovy + + def note_changed_data(self, recurse_up: bool = True): + super().note_changed_data(recurse_up) + self.get_view_matrix(refresh=True) + self.get_implied_camera_location(refresh=True) def set_orientation(self, rotation: Rotation): self.uniforms["orientation"][:] = rotation.as_quat() @@ -77,20 +84,34 @@ class CameraFrame(Mobject): def get_inverse_camera_rotation_matrix(self): return self.get_orientation().as_matrix().T - def get_view_matrix(self): + def get_view_matrix(self, refresh=False): """ Returns a 4x4 for the affine transformation mapping a point into the camera's internal coordinate system """ - shift = Matrix44.from_translation(-self.get_center()).T - rotation = Matrix44.from_quaternion(self.uniforms["orientation"]).T - scale = Matrix44(np.identity(3) / self.get_scale()) - self.view_matrix[:] = shift * rotation * scale + if refresh: + shift = np.identity(4) + rotation = np.identity(4) + scale_mat = np.identity(4) + + shift[:3, 3] = -self.get_center() + rotation[:3, :3] = self.get_inverse_camera_rotation_matrix() + scale = self.get_scale() + if scale > 0: + scale_mat[:3, :3] /= self.get_scale() + + self.view_matrix = np.dot(scale_mat, np.dot(rotation, shift)) + return self.view_matrix def get_inv_view_matrix(self): return np.linalg.inv(self.get_view_matrix()) + @Mobject.affects_data + def interpolate(self, *args, **kwargs): + super().interpolate(*args, **kwargs) + + @Mobject.affects_data def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs): rot = Rotation.from_rotvec(angle * normalize(axis)) self.set_orientation(rot * self.get_orientation()) @@ -181,10 +202,12 @@ class CameraFrame(Mobject): def get_field_of_view(self) -> float: return self.uniforms["fovy"] - def get_implied_camera_location(self) -> np.ndarray: - to_camera = self.get_inverse_camera_rotation_matrix()[2] - dist = self.get_focal_distance() - return self.get_center() + dist * to_camera + def get_implied_camera_location(self, refresh=False) -> np.ndarray: + if refresh: + to_camera = self.get_inverse_camera_rotation_matrix()[2] + dist = self.get_focal_distance() + self.camera_location = self.get_center() + dist * to_camera + return self.camera_location def to_fixed_frame_point(self, point: Vect3, relative: bool = False): view = self.get_view_matrix() diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index f6b27744..4425ac36 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -48,7 +48,7 @@ from manimlib.utils.space_ops import rotation_matrix_transpose from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Callable, Iterable, Union, Tuple, Optional, Self + from typing import Callable, Iterable, Iterator, Union, Tuple, Optional, Self import numpy.typing as npt from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict from moderngl.context import Context @@ -350,7 +350,7 @@ class Mobject(object): return GroupClass(*self.split().__getitem__(value)) return self.split().__getitem__(value) - def __iter__(self) -> Iterable[Self]: + def __iter__(self) -> Iterator[Self]: return iter(self.split()) def __len__(self) -> int: @@ -617,7 +617,6 @@ class Mobject(object): # copy.copy is only a shallow copy, so the internal # data which are numpy arrays or other mobjects still # need to be further copied. - result.data = self.data.copy() result.uniforms = { key: value.copy() if isinstance(value, np.ndarray) else value for key, value in self.uniforms.items() @@ -636,15 +635,14 @@ class Mobject(object): result.non_time_updaters = list(self.non_time_updaters) result.time_based_updaters = list(self.time_based_updaters) result._data_has_changed = True + result._shaders_initialized = False family = self.get_family() for attr, value in self.__dict__.items(): if isinstance(value, Mobject) and value is not self: if value in family: setattr(result, attr, result.family[self.family.index(value)]) - if isinstance(value, np.ndarray): - setattr(result, attr, value.copy()) - if isinstance(value, ShaderWrapper): + elif isinstance(value, np.ndarray): setattr(result, attr, value.copy()) return result @@ -695,6 +693,7 @@ class Mobject(object): sm1.texture_paths = sm2.texture_paths sm1.depth_test = sm2.depth_test sm1.render_primitive = sm2.render_primitive + sm1.needs_new_bounding_box = sm2.needs_new_bounding_box # Make sure named family members carry over for attr, value in list(mobject.__dict__.items()): if isinstance(value, Mobject) and value in family2: @@ -1326,7 +1325,7 @@ class Mobject(object): data = mob.data if mob.has_points() > 0 else mob._data_defaults if color is not None: rgbs = np.array(list(map(color_to_rgb, listify(color)))) - if 1 < len(rgbs) < len(data): + if 1 < len(rgbs): rgbs = resize_with_interpolation(rgbs, len(data)) data[name][:, :3] = rgbs if opacity is not None: @@ -1437,11 +1436,9 @@ class Mobject(object): def add_background_rectangle( self, color: ManimColor | None = None, - opacity: float = 0.75, + opacity: float = 1.0, **kwargs ) -> Self: - # TODO, this does not behave well when the mobject has points, - # since it gets displayed on top from manimlib.mobject.shape_matchers import BackgroundRectangle self.background_rectangle = BackgroundRectangle( self, color=color, diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 0f2764a3..4b0449ae 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -251,8 +251,6 @@ class Laptop(VGroup): self.axis = axis self.add(body, screen_plate, axis) - self.rotate(5 * np.pi / 12, LEFT, about_point=ORIGIN) - self.rotate(np.pi / 6, DOWN, about_point=ORIGIN) class VideoIcon(SVGMobject): diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a4fd7452..8af35fe1 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -890,6 +890,11 @@ class VMobject(Mobject): # Figure out what the subpaths are, and align subpaths1 = self.get_subpaths() subpaths2 = vmobject.get_subpaths() + for subpaths in [subpaths1, subpaths2]: + subpaths.sort(key=lambda sp: -sum( + get_norm(p2 - p1) + for p1, p2 in zip(sp, sp[1:]) + )) n_subpaths = max(len(subpaths1), len(subpaths2)) # Start building new ones @@ -898,7 +903,7 @@ class VMobject(Mobject): def get_nth_subpath(path_list, n): if n >= len(path_list): - return [path_list[-1][-1]] + return np.vstack([path_list[0][:-1], path_list[0][::-1]]) return path_list[n] for n in range(n_subpaths): @@ -922,14 +927,6 @@ class VMobject(Mobject): mob.get_joint_products() return self - def invisible_copy(self) -> Self: - result = self.copy() - if not result.has_fill() or result.get_num_points() == 0: - return result - result.append_vectorized_mobject(self.copy().reverse_points()) - result.set_opacity(0) - return result - def insert_n_curves(self, n: int, recurse: bool = True) -> Self: for mob in self.get_family(recurse): if mob.get_num_curves() > 0: @@ -1223,6 +1220,15 @@ class VMobject(Mobject): self.refresh_joint_products() return self + def set_animating_status(self, is_animating: bool, recurse: bool = True): + super().set_animating_status(is_animating, recurse) + if is_animating: + for submob in self.get_family(recurse): + submob.get_joint_products(refresh=True) + if not submob._use_winding_fill: + submob.get_triangulation() + return self + # For shaders def init_shader_data(self, ctx: Context): dtype = self.shader_dtype @@ -1276,27 +1282,29 @@ class VMobject(Mobject): # Build up data lists fill_datas = [] - fill_border_datas = [] fill_indices = [] + fill_border_datas = [] stroke_datas = [] back_stroke_datas = [] for submob in family: submob.get_joint_products() + indices = submob.get_outer_vert_indices() has_fill = submob.has_fill() has_stroke = submob.has_stroke() - indices = submob.get_outer_vert_indices() - if has_stroke: - lst = back_stroke_datas if submob.stroke_behind else stroke_datas - lst.append(submob.data[stroke_names][indices]) - if has_fill: + back_stroke = has_stroke and submob.stroke_behind + front_stroke = has_stroke and not submob.stroke_behind + if back_stroke: + back_stroke_datas.append(submob.data[stroke_names][indices]) + if front_stroke: + stroke_datas.append(submob.data[stroke_names][indices]) + if has_fill and self._use_winding_fill: data = submob.data[fill_names] data["base_point"][:] = data["point"][0] - if self._use_winding_fill: - fill_datas.append(data[indices]) - else: - fill_datas.append(data) - fill_indices.append(submob.get_triangulation()) - if not has_stroke and has_fill: + fill_datas.append(data[indices]) + if has_fill and not self._use_winding_fill: + fill_datas.append(submob.data[fill_names]) + fill_indices.append(submob.get_triangulation()) + if has_fill and not front_stroke: # Add fill border names = list(stroke_names) names[names.index('stroke_rgba')] = 'fill_rgba' @@ -1307,11 +1315,9 @@ class VMobject(Mobject): fill_border_datas.append(border_stroke_data[indices]) shader_wrappers = [ - self.back_stroke_shader_wrapper.read_in( - [*back_stroke_datas, *fill_border_datas] - ), + self.back_stroke_shader_wrapper.read_in(back_stroke_datas), self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), - self.stroke_shader_wrapper.read_in(stroke_datas), + self.stroke_shader_wrapper.read_in([*fill_border_datas, *stroke_datas]), ] # TODO, account for submob uniforms separately? self.uniforms.update(family[0].uniforms) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 1f522fe7..2c949057 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -280,12 +280,10 @@ class FillShaderWrapper(ShaderWrapper): self.fill_canvas = get_fill_canvas(self.ctx) def render(self): - vao = self.vao - assert(vao is not None) winding = (len(self.vert_indices) == 0) - vao.program['winding'].value = winding + self.program['winding'].value = winding if not winding: - vao.render() + super().render() return original_fbo = self.ctx.fbo @@ -301,14 +299,13 @@ class FillShaderWrapper(ShaderWrapper): gl.GL_ONE, gl.GL_ONE, ) gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX) - self.ctx.blend_equation = moderngl.FUNC_ADD, moderngl.MAX - vao.render() + super().render() original_fbo.use() gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glBlendEquation(gl.GL_FUNC_ADD) - texture_vao.render(moderngl.TRIANGLE_STRIP) + texture_vao.render() gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 81103c18..3297d62b 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -89,7 +89,7 @@ vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw */ float buff = 0.5 * v_stroke_width[index] + aaw; // Add correction for sharp angles to prevent weird bevel effects - if(joint_product.w < -0.9) buff *= 10 * (joint_product.w + 1.0); + if(joint_product.w < -0.75) buff *= 4 * (joint_product.w + 1.0); vec3 normal = get_joint_unit_normal(joint_product); // Set global unit normal unit_normal = normal; diff --git a/manimlib/utils/dict_ops.py b/manimlib/utils/dict_ops.py index 6dcaa7d1..36d27d7b 100644 --- a/manimlib/utils/dict_ops.py +++ b/manimlib/utils/dict_ops.py @@ -1,4 +1,5 @@ import itertools as it +import numpy as np def merge_dicts_recursively(*dicts): @@ -29,3 +30,19 @@ def soft_dict_update(d1, d2): for key, value in list(d2.items()): if key not in d1: d1[key] = value + + +def dict_eq(d1, d2): + if len(d1) != len(d2): + return False + for key in d1: + value1 = d1[key] + value2 = d2[key] + if type(value1) != type(value2): + return False + if type(d1[key]) == np.ndarray: + if any(d1[key] != d2[key]): + return False + elif d1[key] != d2[key]: + return False + return True diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 846c08d5..4f589d5b 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -159,6 +159,7 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu void main() { color = texture(Texture, v_textcoord); + if(color.a == 0) discard; if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard; // Un-blend from the null value @@ -180,5 +181,6 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu simple_program, ctx.buffer(verts.astype('f4').tobytes()), 'texcoord', + mode=moderngl.TRIANGLE_STRIP ) return (texture_fbo, fill_texture_vao, null_rgb)