mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
Merge pull request #1982 from 3b1b/video-work
Various bug fixes and tweaks
This commit is contained in:
commit
c8b65d5621
13 changed files with 107 additions and 87 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()],
|
||||
*[
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue