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]]
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

View file

@ -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)

View file

@ -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:

View file

@ -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()],
*[

View file

@ -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,

View file

@ -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()

View file

@ -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,

View file

@ -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):

View file

@ -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)

View file

@ -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)

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;
// 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;

View file

@ -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

View file

@ -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)