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 Mobject
from manimlib.utils.iterables import remove_list_redundancies
from manimlib.utils.rate_functions import smooth
from manimlib.utils.rate_functions import squish_rate_func
from manimlib.utils.simple_functions import clip
@ -37,7 +38,10 @@ class Animation(object):
remover: bool = False,
# What to enter into the update function upon completion
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.run_time = run_time
@ -65,12 +69,6 @@ class Animation(object):
self.mobject.set_animating_status(True)
self.starting_mobject = self.create_starting_mobject()
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.suspend_updating()
self.families = list(self.get_all_families_zipped())
@ -105,23 +103,19 @@ class Animation(object):
def update_mobjects(self, dt: float) -> None:
"""
Updates things like starting_mobject, and (for
Transforms) target_mobject. Note, since typically
(always?) self.mobject will have its updating
suspended during the animation, this will do
nothing to self.mobject.
Transforms) target_mobject.
"""
for mob in self.get_all_mobjects_to_update():
mob.update(dt)
def get_all_mobjects_to_update(self) -> list[Mobject]:
# The surrounding scene typically handles
# updating of self.mobject. Besides, in
# most cases its updating is suspended anyway
# updating of self.mobject.
items = list(filter(
lambda m: m is not self.mobject,
self.get_all_mobjects()
))
items = list(set(items))
items = remove_list_redundancies(items)
return items
def copy(self):

View file

@ -142,6 +142,7 @@ class Mobject(object):
self.uniforms: UniformDict = {
"is_fixed_in_frame": 0.0,
"shading": np.array(self.shading, dtype=float),
"clip_plane": np.zeros(4),
}
def init_colors(self):
@ -1946,6 +1947,21 @@ class Mobject(object):
mob.depth_test = False
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
@affects_data

View file

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

View file

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

View file

@ -65,10 +65,6 @@ class Surface(Mobject):
)
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]:
# To be implemented in subclasses
return (u, v, 0.0)
@ -216,21 +212,6 @@ class Surface(Mobject):
self.add_updater(updater)
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:
return self.get_triangle_indices()

View file

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

View file

@ -5,7 +5,8 @@ import itertools as it
import numpy as np
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.mobject.geometry import Arrow
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 rgb_to_color
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.simple_functions import sigmoid
from manimlib.utils.space_ops import get_norm
@ -118,7 +120,184 @@ def get_sample_points_from_coordinate_system(
# 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__(
self,
func: Callable[[float, float], Sequence[float]],

View file

@ -507,6 +507,8 @@ class InteractiveScene(Scene):
self.save_selection_to_file()
elif char == "d" and modifiers == SHIFT_MODIFIER:
self.copy_frame_positioning()
elif char == "c" and modifiers == SHIFT_MODIFIER:
self.copy_cursor_position()
elif symbol in ARROW_SYMBOLS:
self.nudge_selection(
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
@ -631,3 +633,6 @@ class InteractiveScene(Scene):
call += ", {:.2f}".format(height)
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)
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
def post_cell_func(*args, **kwargs):
if not self.is_window_closing():

View file

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

View file

@ -2,6 +2,7 @@ uniform float is_fixed_in_frame;
uniform mat4 view;
uniform float focal_distance;
uniform vec3 frame_rescale_factors;
uniform vec4 clip_plane;
void emit_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0);
@ -13,4 +14,8 @@ void emit_gl_Position(vec3 point){
// Flip and scale to prevent premature clipping
result.z *= -0.1;
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
uniform vec4 clip_plane;
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
@ -17,8 +15,4 @@ void main(){
emit_gl_Position(point);
vec3 normal = cross(normalize(du_point - point), normalize(dv_point - point));
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.set_height(label_height)
label.move_to(submob)
label.set_stroke(BLACK, 5, background=True)
label.set_backstroke(BLACK, 5)
labels.add(label)
return labels

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
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
"""
return list(reversed(dict.fromkeys(reversed(lst))))

View file

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