Merge pull request #1982 from 3b1b/video-work

Various bug fixes and tweaks
This commit is contained in:
Grant Sanderson 2023-02-01 11:28:42 -08:00 committed by GitHub
commit c8b65d5621
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 107 additions and 87 deletions

View file

@ -26,12 +26,12 @@ class OpeningManimExample(Scene):
matrix = [[1, 1], [0, 1]] matrix = [[1, 1], [0, 1]]
linear_transform_words = VGroup( linear_transform_words = VGroup(
Text("This is what the matrix"), 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") Text("looks like")
) )
linear_transform_words.arrange(RIGHT) linear_transform_words.arrange(RIGHT)
linear_transform_words.to_edge(UP) linear_transform_words.to_edge(UP)
linear_transform_words.set_stroke(BLACK, 10, background=True) linear_transform_words.set_backstroke(width=5)
self.play( self.play(
ShowCreation(grid), ShowCreation(grid),
@ -52,7 +52,7 @@ class OpeningManimExample(Scene):
this is the map $z \\rightarrow z^2$ this is the map $z \\rightarrow z^2$
""") """)
complex_map_words.to_corner(UR) complex_map_words.to_corner(UR)
complex_map_words.set_stroke(BLACK, 5, background=True) complex_map_words.set_backstroke(width=5)
self.play( self.play(
FadeOut(grid), FadeOut(grid),
@ -268,16 +268,8 @@ class UpdatersExample(Scene):
# that of the newly constructed object # that of the newly constructed object
brace = always_redraw(Brace, square, UP) brace = always_redraw(Brace, square, UP)
text, number = label = VGroup( label = TexText("Width = 0.00")
Text("Width = "), number = label.make_number_changable("0.00")
DecimalNumber(
0,
show_ellipsis=True,
num_decimal_places=2,
include_sign=True,
)
)
label.arrange(RIGHT)
# This ensures that the method deicmal.next_to(square) # This ensures that the method deicmal.next_to(square)
# is called on every frame # is called on every frame

View file

@ -98,11 +98,7 @@ class DrawBorderThenFill(Animation):
self.mobject = vmobject self.mobject = vmobject
def begin(self) -> None: def begin(self) -> None:
# Trigger triangulation calculation self.mobject.set_animating_status(True)
for submob in self.mobject.get_family():
if not submob._use_winding_fill:
submob.get_triangulation()
self.outline = self.get_outline() self.outline = self.get_outline()
super().begin() super().begin()
self.mobject.match_style(self.outline) self.mobject.match_style(self.outline)

View file

@ -70,7 +70,7 @@ class Transform(Animation):
def finish(self) -> None: def finish(self) -> None:
super().finish() super().finish()
self.mobject.unlock_data() 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) self.mobject.become(self.target_mobject)
def create_target(self) -> Mobject: def create_target(self) -> Mobject:

View file

@ -131,9 +131,10 @@ class TransformMatchingStrings(TransformMatchingParts):
target: StringMobject, target: StringMobject,
matched_keys: Iterable[str] = [], matched_keys: Iterable[str] = [],
key_map: dict[str, str] = dict(), key_map: dict[str, str] = dict(),
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
**kwargs, **kwargs,
): ):
matched_pairs = [ matched_pairs = list(matched_pairs) + [
*[(source[key], target[key]) for key in matched_keys], *[(source[key], target[key]) for key in matched_keys],
*[(source[key1], target[key2]) for key1, key2 in key_map.items()], *[(source[key1], target[key2]) for key1, key2 in key_map.items()],
*[ *[

View file

@ -133,20 +133,11 @@ class Camera(object):
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo) gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo)
gl.glBlitFramebuffer( gl.glBlitFramebuffer(
*src_fbo.viewport, *src_fbo.viewport,
*src_fbo.viewport, *dst_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
) )
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes: 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) self.blit(self.fbo, self.draw_fbo)
return self.draw_fbo.read( return self.draw_fbo.read(
viewport=self.draw_fbo.viewport, viewport=self.draw_fbo.viewport,

View file

@ -29,15 +29,22 @@ class CameraFrame(Mobject):
): ):
super().__init__(**kwargs) 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.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_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
self.set_width(frame_shape[0], stretch=True) self.set_width(frame_shape[0], stretch=True)
self.set_height(frame_shape[1], stretch=True) self.set_height(frame_shape[1], stretch=True)
self.move_to(center_point) 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): def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"][:] = rotation.as_quat() self.uniforms["orientation"][:] = rotation.as_quat()
@ -77,20 +84,34 @@ class CameraFrame(Mobject):
def get_inverse_camera_rotation_matrix(self): def get_inverse_camera_rotation_matrix(self):
return self.get_orientation().as_matrix().T 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 Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system into the camera's internal coordinate system
""" """
shift = Matrix44.from_translation(-self.get_center()).T if refresh:
rotation = Matrix44.from_quaternion(self.uniforms["orientation"]).T shift = np.identity(4)
scale = Matrix44(np.identity(3) / self.get_scale()) rotation = np.identity(4)
self.view_matrix[:] = shift * rotation * scale 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 return self.view_matrix
def get_inv_view_matrix(self): def get_inv_view_matrix(self):
return np.linalg.inv(self.get_view_matrix()) 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): def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
rot = Rotation.from_rotvec(angle * normalize(axis)) rot = Rotation.from_rotvec(angle * normalize(axis))
self.set_orientation(rot * self.get_orientation()) self.set_orientation(rot * self.get_orientation())
@ -181,10 +202,12 @@ class CameraFrame(Mobject):
def get_field_of_view(self) -> float: def get_field_of_view(self) -> float:
return self.uniforms["fovy"] return self.uniforms["fovy"]
def get_implied_camera_location(self) -> np.ndarray: def get_implied_camera_location(self, refresh=False) -> np.ndarray:
if refresh:
to_camera = self.get_inverse_camera_rotation_matrix()[2] to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance() dist = self.get_focal_distance()
return self.get_center() + dist * to_camera self.camera_location = self.get_center() + dist * to_camera
return self.camera_location
def to_fixed_frame_point(self, point: Vect3, relative: bool = False): def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
view = self.get_view_matrix() view = self.get_view_matrix()

View file

@ -48,7 +48,7 @@ from manimlib.utils.space_ops import rotation_matrix_transpose
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if 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 import numpy.typing as npt
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict
from moderngl.context import Context from moderngl.context import Context
@ -350,7 +350,7 @@ class Mobject(object):
return GroupClass(*self.split().__getitem__(value)) return GroupClass(*self.split().__getitem__(value))
return self.split().__getitem__(value) return self.split().__getitem__(value)
def __iter__(self) -> Iterable[Self]: def __iter__(self) -> Iterator[Self]:
return iter(self.split()) return iter(self.split())
def __len__(self) -> int: def __len__(self) -> int:
@ -617,7 +617,6 @@ class Mobject(object):
# copy.copy is only a shallow copy, so the internal # copy.copy is only a shallow copy, so the internal
# data which are numpy arrays or other mobjects still # data which are numpy arrays or other mobjects still
# need to be further copied. # need to be further copied.
result.data = self.data.copy()
result.uniforms = { result.uniforms = {
key: value.copy() if isinstance(value, np.ndarray) else value key: value.copy() if isinstance(value, np.ndarray) else value
for key, value in self.uniforms.items() for key, value in self.uniforms.items()
@ -636,15 +635,14 @@ class Mobject(object):
result.non_time_updaters = list(self.non_time_updaters) result.non_time_updaters = list(self.non_time_updaters)
result.time_based_updaters = list(self.time_based_updaters) result.time_based_updaters = list(self.time_based_updaters)
result._data_has_changed = True result._data_has_changed = True
result._shaders_initialized = False
family = self.get_family() family = self.get_family()
for attr, value in self.__dict__.items(): for attr, value in self.__dict__.items():
if isinstance(value, Mobject) and value is not self: if isinstance(value, Mobject) and value is not self:
if value in family: if value in family:
setattr(result, attr, result.family[self.family.index(value)]) setattr(result, attr, result.family[self.family.index(value)])
if isinstance(value, np.ndarray): elif isinstance(value, np.ndarray):
setattr(result, attr, value.copy())
if isinstance(value, ShaderWrapper):
setattr(result, attr, value.copy()) setattr(result, attr, value.copy())
return result return result
@ -695,6 +693,7 @@ class Mobject(object):
sm1.texture_paths = sm2.texture_paths sm1.texture_paths = sm2.texture_paths
sm1.depth_test = sm2.depth_test sm1.depth_test = sm2.depth_test
sm1.render_primitive = sm2.render_primitive sm1.render_primitive = sm2.render_primitive
sm1.needs_new_bounding_box = sm2.needs_new_bounding_box
# Make sure named family members carry over # Make sure named family members carry over
for attr, value in list(mobject.__dict__.items()): for attr, value in list(mobject.__dict__.items()):
if isinstance(value, Mobject) and value in family2: 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 data = mob.data if mob.has_points() > 0 else mob._data_defaults
if color is not None: if color is not None:
rgbs = np.array(list(map(color_to_rgb, listify(color)))) 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)) rgbs = resize_with_interpolation(rgbs, len(data))
data[name][:, :3] = rgbs data[name][:, :3] = rgbs
if opacity is not None: if opacity is not None:
@ -1437,11 +1436,9 @@ class Mobject(object):
def add_background_rectangle( def add_background_rectangle(
self, self,
color: ManimColor | None = None, color: ManimColor | None = None,
opacity: float = 0.75, opacity: float = 1.0,
**kwargs **kwargs
) -> Self: ) -> 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 from manimlib.mobject.shape_matchers import BackgroundRectangle
self.background_rectangle = BackgroundRectangle( self.background_rectangle = BackgroundRectangle(
self, color=color, self, color=color,

View file

@ -251,8 +251,6 @@ class Laptop(VGroup):
self.axis = axis self.axis = axis
self.add(body, screen_plate, 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): class VideoIcon(SVGMobject):

View file

@ -890,6 +890,11 @@ class VMobject(Mobject):
# Figure out what the subpaths are, and align # Figure out what the subpaths are, and align
subpaths1 = self.get_subpaths() subpaths1 = self.get_subpaths()
subpaths2 = vmobject.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)) n_subpaths = max(len(subpaths1), len(subpaths2))
# Start building new ones # Start building new ones
@ -898,7 +903,7 @@ class VMobject(Mobject):
def get_nth_subpath(path_list, n): def get_nth_subpath(path_list, n):
if n >= len(path_list): 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] return path_list[n]
for n in range(n_subpaths): for n in range(n_subpaths):
@ -922,14 +927,6 @@ class VMobject(Mobject):
mob.get_joint_products() mob.get_joint_products()
return self 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: def insert_n_curves(self, n: int, recurse: bool = True) -> Self:
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
if mob.get_num_curves() > 0: if mob.get_num_curves() > 0:
@ -1223,6 +1220,15 @@ class VMobject(Mobject):
self.refresh_joint_products() self.refresh_joint_products()
return self 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 # For shaders
def init_shader_data(self, ctx: Context): def init_shader_data(self, ctx: Context):
dtype = self.shader_dtype dtype = self.shader_dtype
@ -1276,27 +1282,29 @@ class VMobject(Mobject):
# Build up data lists # Build up data lists
fill_datas = [] fill_datas = []
fill_border_datas = []
fill_indices = [] fill_indices = []
fill_border_datas = []
stroke_datas = [] stroke_datas = []
back_stroke_datas = [] back_stroke_datas = []
for submob in family: for submob in family:
submob.get_joint_products() submob.get_joint_products()
indices = submob.get_outer_vert_indices()
has_fill = submob.has_fill() has_fill = submob.has_fill()
has_stroke = submob.has_stroke() has_stroke = submob.has_stroke()
indices = submob.get_outer_vert_indices() back_stroke = has_stroke and submob.stroke_behind
if has_stroke: front_stroke = has_stroke and not submob.stroke_behind
lst = back_stroke_datas if submob.stroke_behind else stroke_datas if back_stroke:
lst.append(submob.data[stroke_names][indices]) back_stroke_datas.append(submob.data[stroke_names][indices])
if has_fill: 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 = submob.data[fill_names]
data["base_point"][:] = data["point"][0] data["base_point"][:] = data["point"][0]
if self._use_winding_fill:
fill_datas.append(data[indices]) fill_datas.append(data[indices])
else: if has_fill and not self._use_winding_fill:
fill_datas.append(data) fill_datas.append(submob.data[fill_names])
fill_indices.append(submob.get_triangulation()) fill_indices.append(submob.get_triangulation())
if not has_stroke and has_fill: if has_fill and not front_stroke:
# Add fill border # Add fill border
names = list(stroke_names) names = list(stroke_names)
names[names.index('stroke_rgba')] = 'fill_rgba' names[names.index('stroke_rgba')] = 'fill_rgba'
@ -1307,11 +1315,9 @@ class VMobject(Mobject):
fill_border_datas.append(border_stroke_data[indices]) fill_border_datas.append(border_stroke_data[indices])
shader_wrappers = [ shader_wrappers = [
self.back_stroke_shader_wrapper.read_in( self.back_stroke_shader_wrapper.read_in(back_stroke_datas),
[*back_stroke_datas, *fill_border_datas]
),
self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), 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? # TODO, account for submob uniforms separately?
self.uniforms.update(family[0].uniforms) self.uniforms.update(family[0].uniforms)

View file

@ -280,12 +280,10 @@ class FillShaderWrapper(ShaderWrapper):
self.fill_canvas = get_fill_canvas(self.ctx) self.fill_canvas = get_fill_canvas(self.ctx)
def render(self): def render(self):
vao = self.vao
assert(vao is not None)
winding = (len(self.vert_indices) == 0) winding = (len(self.vert_indices) == 0)
vao.program['winding'].value = winding self.program['winding'].value = winding
if not winding: if not winding:
vao.render() super().render()
return return
original_fbo = self.ctx.fbo original_fbo = self.ctx.fbo
@ -301,14 +299,13 @@ class FillShaderWrapper(ShaderWrapper):
gl.GL_ONE, gl.GL_ONE, gl.GL_ONE, gl.GL_ONE,
) )
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX) 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() original_fbo.use()
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
gl.glBlendEquation(gl.GL_FUNC_ADD) 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) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)

View file

@ -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; float buff = 0.5 * v_stroke_width[index] + aaw;
// Add correction for sharp angles to prevent weird bevel effects // 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); vec3 normal = get_joint_unit_normal(joint_product);
// Set global unit normal // Set global unit normal
unit_normal = normal; unit_normal = normal;

View file

@ -1,4 +1,5 @@
import itertools as it import itertools as it
import numpy as np
def merge_dicts_recursively(*dicts): def merge_dicts_recursively(*dicts):
@ -29,3 +30,19 @@ def soft_dict_update(d1, d2):
for key, value in list(d2.items()): for key, value in list(d2.items()):
if key not in d1: if key not in d1:
d1[key] = value 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

View file

@ -159,6 +159,7 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu
void main() { void main() {
color = texture(Texture, v_textcoord); color = texture(Texture, v_textcoord);
if(color.a == 0) discard;
if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard; if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard;
// Un-blend from the null value // Un-blend from the null value
@ -180,5 +181,6 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tu
simple_program, simple_program,
ctx.buffer(verts.astype('f4').tobytes()), ctx.buffer(verts.astype('f4').tobytes()),
'texcoord', 'texcoord',
mode=moderngl.TRIANGLE_STRIP
) )
return (texture_fbo, fill_texture_vao, null_rgb) return (texture_fbo, fill_texture_vao, null_rgb)