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

Video work
This commit is contained in:
Grant Sanderson 2024-10-10 12:23:19 -07:00 committed by GitHub
commit 15e5d8a07a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 234 additions and 63 deletions

View file

@ -4,6 +4,7 @@ from copy import deepcopy
from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import _AnimationBuilder
from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Mobject
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.rate_functions import smooth from manimlib.utils.rate_functions import smooth
from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.rate_functions import squish_rate_func
from manimlib.utils.simple_functions import clip from manimlib.utils.simple_functions import clip
@ -37,7 +38,10 @@ class Animation(object):
remover: bool = False, remover: bool = False,
# What to enter into the update function upon completion # What to enter into the update function upon completion
final_alpha_value: float = 1.0, final_alpha_value: float = 1.0,
suspend_mobject_updating: bool = True, # If set to True, the mobject itself will have its internal updaters called,
# but the start or target mobjects would not be suspended. To completely suspend
# updating, call mobject.suspend_updating() before the animation
suspend_mobject_updating: bool = False,
): ):
self.mobject = mobject self.mobject = mobject
self.run_time = run_time self.run_time = run_time
@ -65,12 +69,6 @@ class Animation(object):
self.mobject.set_animating_status(True) self.mobject.set_animating_status(True)
self.starting_mobject = self.create_starting_mobject() self.starting_mobject = self.create_starting_mobject()
if self.suspend_mobject_updating: if self.suspend_mobject_updating:
# All calls to self.mobject's internal updaters
# during the animation, either from this Animation
# or from the surrounding scene, should do nothing.
# It is, however, okay and desirable to call
# the internal updaters of self.starting_mobject,
# or any others among self.get_all_mobjects()
self.mobject_was_updating = not self.mobject.updating_suspended self.mobject_was_updating = not self.mobject.updating_suspended
self.mobject.suspend_updating() self.mobject.suspend_updating()
self.families = list(self.get_all_families_zipped()) self.families = list(self.get_all_families_zipped())
@ -105,23 +103,19 @@ class Animation(object):
def update_mobjects(self, dt: float) -> None: def update_mobjects(self, dt: float) -> None:
""" """
Updates things like starting_mobject, and (for Updates things like starting_mobject, and (for
Transforms) target_mobject. Note, since typically Transforms) target_mobject.
(always?) self.mobject will have its updating
suspended during the animation, this will do
nothing to self.mobject.
""" """
for mob in self.get_all_mobjects_to_update(): for mob in self.get_all_mobjects_to_update():
mob.update(dt) mob.update(dt)
def get_all_mobjects_to_update(self) -> list[Mobject]: def get_all_mobjects_to_update(self) -> list[Mobject]:
# The surrounding scene typically handles # The surrounding scene typically handles
# updating of self.mobject. Besides, in # updating of self.mobject.
# most cases its updating is suspended anyway
items = list(filter( items = list(filter(
lambda m: m is not self.mobject, lambda m: m is not self.mobject,
self.get_all_mobjects() self.get_all_mobjects()
)) ))
items = list(set(items)) items = remove_list_redundancies(items)
return items return items
def copy(self): def copy(self):

View file

@ -142,6 +142,7 @@ class Mobject(object):
self.uniforms: UniformDict = { self.uniforms: UniformDict = {
"is_fixed_in_frame": 0.0, "is_fixed_in_frame": 0.0,
"shading": np.array(self.shading, dtype=float), "shading": np.array(self.shading, dtype=float),
"clip_plane": np.zeros(4),
} }
def init_colors(self): def init_colors(self):
@ -1946,6 +1947,21 @@ class Mobject(object):
mob.depth_test = False mob.depth_test = False
return self return self
def set_clip_plane(
self,
vect: Vect3 | None = None,
threshold: float | None = None
) -> Self:
if vect is not None:
self.uniforms["clip_plane"][:3] = vect
if threshold is not None:
self.uniforms["clip_plane"][3] = threshold
return self
def deactivate_clip_plane(self) -> Self:
self.uniforms["clip_plane"][:] = 0
return self
# Shader code manipulation # Shader code manipulation
@affects_data @affects_data

View file

@ -15,7 +15,6 @@ if TYPE_CHECKING:
from manimlib.typing import ManimColor, Vect3 from manimlib.typing import ManimColor, Vect3
class BulletedList(VGroup): class BulletedList(VGroup):
def __init__( def __init__(
self, self,

View file

@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
import numpy as np import numpy as np
import moderngl
from PIL import Image from PIL import Image
from moderngl import TRIANGLES
from manimlib.constants import DL, DR, UL, UR from manimlib.constants import DL, DR, UL, UR
from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Mobject
@ -25,7 +25,7 @@ class ImageMobject(Mobject):
('im_coords', np.float32, (2,)), ('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)), ('opacity', np.float32, (1,)),
] ]
render_primitive: int = TRIANGLES render_primitive: int = moderngl.TRIANGLES
def __init__( def __init__(
self, self,

View file

@ -65,10 +65,6 @@ class Surface(Mobject):
) )
self.compute_triangle_indices() self.compute_triangle_indices()
def init_uniforms(self):
super().init_uniforms()
self.uniforms["clip_plane"] = np.zeros(4)
def uv_func(self, u: float, v: float) -> tuple[float, float, float]: def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
# To be implemented in subclasses # To be implemented in subclasses
return (u, v, 0.0) return (u, v, 0.0)
@ -216,21 +212,6 @@ class Surface(Mobject):
self.add_updater(updater) self.add_updater(updater)
return self return self
def set_clip_plane(
self,
vect: Vect3 | None = None,
threshold: float | None = None
) -> Self:
if vect is not None:
self.uniforms["clip_plane"][:3] = vect
if threshold is not None:
self.uniforms["clip_plane"][3] = threshold
return self
def deactivate_clip_plane(self) -> Self:
self.uniforms["clip_plane"][:] = 0
return self
def get_shader_vert_indices(self) -> np.ndarray: def get_shader_vert_indices(self) -> np.ndarray:
return self.get_triangle_indices() return self.get_triangle_indices()

View file

@ -767,6 +767,8 @@ class VMobject(Mobject):
def quick_point_from_proportion(self, alpha: float) -> Vect3: def quick_point_from_proportion(self, alpha: float) -> Vect3:
# Assumes all curves have the same length, so is inaccurate # Assumes all curves have the same length, so is inaccurate
num_curves = self.get_num_curves() num_curves = self.get_num_curves()
if num_curves == 0:
return self.get_center()
n, residue = integer_interpolate(0, num_curves, alpha) n, residue = integer_interpolate(0, num_curves, alpha)
curve_func = self.get_nth_curve_function(n) curve_func = self.get_nth_curve_function(n)
return curve_func(residue) return curve_func(residue)

View file

@ -5,7 +5,8 @@ import itertools as it
import numpy as np import numpy as np
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import WHITE from manimlib.constants import BLUE, WHITE
from manimlib.constants import ORIGIN
from manimlib.animation.indication import VShowPassingFlash from manimlib.animation.indication import VShowPassingFlash
from manimlib.mobject.geometry import Arrow from manimlib.mobject.geometry import Arrow
from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VGroup
@ -15,6 +16,7 @@ from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.color import get_colormap_list from manimlib.utils.color import get_colormap_list
from manimlib.utils.color import rgb_to_color from manimlib.utils.color import rgb_to_color
from manimlib.utils.dict_ops import merge_dicts_recursively from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.iterables import cartesian_product
from manimlib.utils.rate_functions import linear from manimlib.utils.rate_functions import linear
from manimlib.utils.simple_functions import sigmoid from manimlib.utils.simple_functions import sigmoid
from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import get_norm
@ -118,7 +120,184 @@ def get_sample_points_from_coordinate_system(
# Mobjects # Mobjects
class VectorField(VGroup):
class VectorField(VMobject):
def __init__(
self,
func,
stroke_color: ManimColor = BLUE,
stroke_opacity: float = 1.0,
center: Vect3 = ORIGIN,
sample_points: Optional[Vect3Array] = None,
x_density: float = 2.0,
y_density: float = 2.0,
z_density: float = 2.0,
width: float = 14.0,
height: float = 8.0,
depth: float = 0.0,
stroke_width: float = 2,
tip_width_ratio: float = 4,
tip_len_to_width: float = 0.01,
max_vect_len: float | None = None,
min_drawn_norm: float = 1e-2,
flat_stroke: bool = False,
norm_to_opacity_func=None,
norm_to_rgb_func=None,
**kwargs
):
self.func = func
self.stroke_width = stroke_width
self.tip_width_ratio = tip_width_ratio
self.tip_len_to_width = tip_len_to_width
self.min_drawn_norm = min_drawn_norm
self.norm_to_opacity_func = norm_to_opacity_func
self.norm_to_rgb_func = norm_to_rgb_func
if max_vect_len is not None:
self.max_vect_len = max_vect_len
else:
densities = np.array([x_density, y_density, z_density])
dims = np.array([width, height, depth])
self.max_vect_len = 1.0 / densities[dims > 0].mean()
if sample_points is None:
self.sample_points = self.get_sample_points(
center, width, height, depth,
x_density, y_density, z_density
)
else:
self.sample_points = sample_points
self.init_base_stroke_width_array(len(self.sample_points))
super().__init__(
stroke_color=stroke_color,
stroke_opacity=stroke_opacity,
flat_stroke=flat_stroke,
**kwargs
)
n_samples = len(self.sample_points)
self.set_points(np.zeros((8 * n_samples - 1, 3)))
self.set_stroke(width=stroke_width)
self.set_joint_type('no_joint')
self.update_vectors()
def get_sample_points(
self,
center: np.ndarray,
width: float,
height: float,
depth: float,
x_density: float,
y_density: float,
z_density: float
) -> np.ndarray:
to_corner = np.array([width / 2, height / 2, depth / 2])
spacings = 1.0 / np.array([x_density, y_density, z_density])
to_corner = spacings * (to_corner / spacings).astype(int)
lower_corner = center - to_corner
upper_corner = center + to_corner + spacings
return cartesian_product(*(
np.arange(low, high, space)
for low, high, space in zip(lower_corner, upper_corner, spacings)
))
def init_base_stroke_width_array(self, n_sample_points):
arr = np.ones(8 * n_sample_points - 1)
arr[4::8] = self.tip_width_ratio
arr[5::8] = self.tip_width_ratio * 0.5
arr[6::8] = 0
arr[7::8] = 0
self.base_stroke_width_array = arr
def set_sample_points(self, sample_points: Vect3Array):
self.sample_points = sample_points
return self
def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True):
super().set_stroke(color, None, opacity, behind, flat, recurse)
if width is not None:
self.set_stroke_width(float(width))
return self
def set_stroke_width(self, width: float):
if self.get_num_points() > 0:
self.get_stroke_widths()[:] = width * self.base_stroke_width_array
self.stroke_width = width
return self
def update_vectors(self):
tip_width = self.tip_width_ratio * self.stroke_width
tip_len = self.tip_len_to_width * tip_width
samples = self.sample_points
# Get raw outputs and lengths
outputs = self.func(samples)
norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
# How long should the arrows be drawn?
max_len = self.max_vect_len
if max_len < np.inf:
drawn_norms = max_len * np.tanh(norms / max_len)
else:
drawn_norms = norms
# What's the distance from the base of an arrow to
# the base of its head?
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf)
# Set all points
unit_outputs = np.zeros_like(outputs)
np.true_divide(outputs, norms, out=unit_outputs, where=(norms > self.min_drawn_norm))
points = self.get_points()
points[0::8] = samples
points[2::8] = samples + dist_to_head_base * unit_outputs
points[4::8] = points[2::8]
points[6::8] = samples + drawn_norms * unit_outputs
for i in (1, 3, 5):
points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8])
points[7::8] = points[6:-1:8]
# Adjust stroke widths
width_arr = self.stroke_width * self.base_stroke_width_array
width_scalars = np.clip(drawn_norms / tip_len, 0, 1)
width_scalars = np.repeat(width_scalars, 8)[:-1]
self.get_stroke_widths()[:] = width_scalars * width_arr
# Potentially adjust opacity and color
if self.norm_to_opacity_func is not None:
self.get_stroke_opacities()[:] = self.norm_to_opacity_func(
np.repeat(norms, 8)[:-1]
)
if self.norm_to_rgb_func is not None:
self.get_stroke_colors()
self.data['stroke_rgba'][:, :3] = self.norm_to_rgb_func(
np.repeat(norms, 8)[:-1]
)
self.note_changed_data()
return self
class TimeVaryingVectorField(VectorField):
def __init__(
self,
# Takes in an array of points and a float for time
time_func,
**kwargs
):
self.time = 0
super().__init__(func=lambda p: time_func(p, self.time), **kwargs)
self.add_updater(lambda m, dt: m.increment_time(dt))
always(self.update_vectors)
def increment_time(self, dt):
self.time += dt
class OldVectorField(VGroup):
def __init__( def __init__(
self, self,
func: Callable[[float, float], Sequence[float]], func: Callable[[float, float], Sequence[float]],

View file

@ -507,6 +507,8 @@ class InteractiveScene(Scene):
self.save_selection_to_file() self.save_selection_to_file()
elif char == "d" and modifiers == SHIFT_MODIFIER: elif char == "d" and modifiers == SHIFT_MODIFIER:
self.copy_frame_positioning() self.copy_frame_positioning()
elif char == "c" and modifiers == SHIFT_MODIFIER:
self.copy_cursor_position()
elif symbol in ARROW_SYMBOLS: elif symbol in ARROW_SYMBOLS:
self.nudge_selection( self.nudge_selection(
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)], vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
@ -631,3 +633,6 @@ class InteractiveScene(Scene):
call += ", {:.2f}".format(height) call += ", {:.2f}".format(height)
call += ")" call += ")"
pyperclip.copy(call) pyperclip.copy(call)
def copy_cursor_position(self):
pyperclip.copy(str(tuple(self.mouse_point.get_center().round(2))))

View file

@ -255,17 +255,6 @@ class Scene(object):
pt_inputhooks.register("manim", inputhook) pt_inputhooks.register("manim", inputhook)
shell.enable_gui("manim") shell.enable_gui("manim")
# This is hacky, but there's an issue with ipython which is that
# when you define lambda's or list comprehensions during a shell session,
# they are not aware of local variables in the surrounding scope. Because
# That comes up a fair bit during scene construction, to get around this,
# we (admittedly sketchily) update the global namespace to match the local
# namespace, since this is just a shell session anyway.
shell.events.register(
"pre_run_cell",
lambda *args, **kwargs: shell.user_global_ns.update(shell.user_ns)
)
# Operation to run after each ipython command # Operation to run after each ipython command
def post_cell_func(*args, **kwargs): def post_cell_func(*args, **kwargs):
if not self.is_window_closing(): if not self.is_window_closing():

View file

@ -9,6 +9,7 @@ import sys
import numpy as np import numpy as np
from pydub import AudioSegment from pydub import AudioSegment
from tqdm.auto import tqdm as ProgressDisplay from tqdm.auto import tqdm as ProgressDisplay
from pathlib import Path
from manimlib.constants import FFMPEG_BIN from manimlib.constants import FFMPEG_BIN
from manimlib.logger import log from manimlib.logger import log
@ -299,15 +300,21 @@ class SceneFileWriter(object):
self.video_codec = "libx264rgb" self.video_codec = "libx264rgb"
self.pixel_format = "rgb32" self.pixel_format = "rgb32"
def get_insert_file_path(self, index: int) -> Path:
movie_path = Path(self.get_movie_file_path())
scene_name = movie_path.stem
insert_dir = Path(movie_path.parent, "inserts")
guarantee_existence(str(insert_dir))
return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}")
def begin_insert(self): def begin_insert(self):
# Begin writing process # Begin writing process
self.write_to_movie = True self.write_to_movie = True
self.init_output_directories() self.init_output_directories()
movie_path = self.get_movie_file_path() index = 0
count = 0 while (insert_path := self.get_insert_file_path(index)).exists():
while os.path.exists(name := movie_path.replace(".", f"_insert_{count}.")): index += 1
count += 1 self.inserted_file_path = str(insert_path)
self.inserted_file_path = name
self.open_movie_pipe(self.inserted_file_path) self.open_movie_pipe(self.inserted_file_path)
def end_insert(self): def end_insert(self):

View file

@ -2,6 +2,7 @@ uniform float is_fixed_in_frame;
uniform mat4 view; uniform mat4 view;
uniform float focal_distance; uniform float focal_distance;
uniform vec3 frame_rescale_factors; uniform vec3 frame_rescale_factors;
uniform vec4 clip_plane;
void emit_gl_Position(vec3 point){ void emit_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0); vec4 result = vec4(point, 1.0);
@ -13,4 +14,8 @@ void emit_gl_Position(vec3 point){
// Flip and scale to prevent premature clipping // Flip and scale to prevent premature clipping
result.z *= -0.1; result.z *= -0.1;
gl_Position = result; gl_Position = result;
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
}
} }

View file

@ -1,7 +1,5 @@
#version 330 #version 330
uniform vec4 clip_plane;
in vec3 point; in vec3 point;
in vec3 du_point; in vec3 du_point;
in vec3 dv_point; in vec3 dv_point;
@ -17,8 +15,4 @@ void main(){
emit_gl_Position(point); emit_gl_Position(point);
vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point)); vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point));
v_color = finalize_color(rgba, point, normalize(normal)); v_color = finalize_color(rgba, point, normalize(normal));
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
}
} }

View file

@ -27,6 +27,6 @@ def index_labels(
label = Integer(n) label = Integer(n)
label.set_height(label_height) label.set_height(label_height)
label.move_to(submob) label.move_to(submob)
label.set_stroke(BLACK, 5, background=True) label.set_backstroke(BLACK, 5)
labels.add(label) labels.add(label)
return labels return labels

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
def remove_list_redundancies(lst: Sequence[T]) -> list[T]: def remove_list_redundancies(lst: Sequence[T]) -> list[T]:
""" """
Used instead of list(set(l)) to maintain order Remove duplicate elements while preserving order.
Keeps the last occurrence of each element Keeps the last occurrence of each element
""" """
return list(reversed(dict.fromkeys(reversed(lst)))) return list(reversed(dict.fromkeys(reversed(lst))))

View file

@ -67,7 +67,7 @@ def set_program_uniform(
uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid] uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid]
if type(value) is np.ndarray and value.ndim > 0: if type(value) is np.ndarray and value.ndim > 0:
value = tuple(value) value = tuple(value.flatten())
if uniform_mirror.get(name, None) == value: if uniform_mirror.get(name, None) == value:
return False return False