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

Video work
This commit is contained in:
Grant Sanderson
2023-01-25 09:54:55 -08:00
committed by GitHub
9 changed files with 169 additions and 80 deletions

View File

@ -26,6 +26,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.shader_wrapper import ShaderWrapper
from manimlib.typing import ManimColor, Vect3
from manimlib.window import Window
from typing import Any, Iterable
class CameraFrame(Mobject):
@ -39,7 +40,7 @@ class CameraFrame(Mobject):
self.frame_shape = frame_shape
self.center_point = center_point
self.focal_dist_to_height = focal_dist_to_height
self.perspective_transform = np.identity(4)
self.view_matrix = np.identity(4)
super().__init__(**kwargs)
def init_uniforms(self) -> None:
@ -83,12 +84,12 @@ class CameraFrame(Mobject):
def get_inverse_camera_rotation_matrix(self):
return self.get_orientation().as_matrix().T
def get_perspective_transform(self):
def get_view_matrix(self):
"""
Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system
"""
result = self.perspective_transform
result = self.view_matrix
result[:] = np.identity(4)
result[:3, 3] = -self.get_center()
rotation = np.identity(4)
@ -187,7 +188,7 @@ class CameraFrame(Mobject):
class Camera(object):
def __init__(
self,
ctx: moderngl.Context | None = None,
window: Window | None = None,
background_image: str | None = None,
frame_config: dict = dict(),
pixel_width: int = DEFAULT_PIXEL_WIDTH,
@ -209,8 +210,8 @@ class Camera(object):
samples: int = 0,
):
self.background_image = background_image
self.pixel_width = pixel_width
self.pixel_height = pixel_height
self.window = window
self.default_pixel_shape = (pixel_width, pixel_height)
self.fps = fps
self.max_allowable_norm = max_allowable_norm
self.image_mode = image_mode
@ -225,7 +226,7 @@ class Camera(object):
))
self.perspective_uniforms = dict()
self.init_frame(**frame_config)
self.init_context(ctx)
self.init_context(window)
self.init_shaders()
self.init_textures()
self.init_light_source()
@ -238,21 +239,20 @@ class Camera(object):
def init_frame(self, **config) -> None:
self.frame = CameraFrame(**config)
def init_context(self, ctx: moderngl.Context | None = None) -> None:
if ctx is None:
ctx = moderngl.create_standalone_context()
fbo = self.get_fbo(ctx, self.samples)
def init_context(self, window: Window | None = None) -> None:
if window is None:
self.ctx = moderngl.create_standalone_context()
self.fbo = self.get_fbo(self.samples)
else:
fbo = ctx.detect_framebuffer()
self.ctx = ctx
self.fbo = fbo
self.ctx = window.ctx
self.fbo = self.ctx.detect_framebuffer()
self.fbo.use()
self.set_ctx_blending()
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
# This is the frame buffer we'll draw into when emitting frames
self.draw_fbo = self.get_fbo(ctx, 0)
self.draw_fbo = self.get_fbo(samples=0)
def set_ctx_blending(self, enable: bool = True) -> None:
if enable:
@ -269,8 +269,6 @@ class Camera(object):
def set_ctx_clip_plane(self, enable: bool = True) -> None:
if enable:
gl.glEnable(gl.GL_CLIP_DISTANCE0)
else:
gl.glDisable(gl.GL_CLIP_DISTANCE0)
def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position)
@ -278,19 +276,16 @@ class Camera(object):
# Methods associated with the frame buffer
def get_fbo(
self,
ctx: moderngl.Context,
samples: int = 0
) -> moderngl.Framebuffer:
pw = self.pixel_width
ph = self.pixel_height
return ctx.framebuffer(
color_attachments=ctx.texture(
(pw, ph),
return self.ctx.framebuffer(
color_attachments=self.ctx.texture(
self.default_pixel_shape,
components=self.n_channels,
samples=samples,
),
depth_attachment=ctx.depth_renderbuffer(
(pw, ph),
depth_attachment=self.ctx.depth_renderbuffer(
self.default_pixel_shape,
samples=samples
)
)
@ -298,17 +293,19 @@ class Camera(object):
def clear(self) -> None:
self.fbo.clear(*self.background_rgba)
def reset_pixel_shape(self, new_width: int, new_height: int) -> None:
self.pixel_width = new_width
self.pixel_height = new_height
self.refresh_perspective_uniforms()
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
# Copy blocks from fbo into draw_fbo using Blit
pw, ph = (self.pixel_width, self.pixel_height)
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo)
gl.glBlitFramebuffer(0, 0, pw, ph, 0, 0, pw, ph, gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR)
if self.window is not None:
src_viewport = self.window.viewport
else:
src_viewport = self.fbo.viewport
gl.glBlitFramebuffer(
*src_viewport,
*self.draw_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
)
return self.draw_fbo.read(
viewport=self.draw_fbo.viewport,
components=self.n_channels,
@ -326,7 +323,7 @@ class Camera(object):
def get_pixel_array(self) -> np.ndarray:
raw = self.get_raw_fbo_data(dtype='f4')
flat_arr = np.frombuffer(raw, dtype='f4')
arr = flat_arr.reshape([*reversed(self.fbo.size), self.n_channels])
arr = flat_arr.reshape([*reversed(self.draw_fbo.size), self.n_channels])
arr = arr[::-1]
# Convert from float
return (self.rgb_max_val * arr).astype(self.pixel_array_dtype)
@ -342,9 +339,11 @@ class Camera(object):
return texture
# Getting camera attributes
def get_pixel_size(self) -> float:
return self.frame.get_shape()[0] / self.get_pixel_shape()[0]
def get_pixel_shape(self) -> tuple[int, int]:
return self.fbo.viewport[2:4]
# return (self.pixel_width, self.pixel_height)
return self.draw_fbo.size
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
@ -352,6 +351,10 @@ class Camera(object):
def get_pixel_height(self) -> int:
return self.get_pixel_shape()[1]
def get_aspect_ratio(self):
pw, ph = self.get_pixel_shape()
return pw / ph
def get_frame_height(self) -> float:
return self.frame.get_height()
@ -374,17 +377,15 @@ class Camera(object):
whether frame_height or frame_width
remains fixed while the other changes accordingly.
"""
pixel_height = self.get_pixel_height()
pixel_width = self.get_pixel_width()
frame_height = self.get_frame_height()
frame_width = self.get_frame_width()
aspect_ratio = fdiv(pixel_width, pixel_height)
aspect_ratio = self.get_aspect_ratio()
if not fixed_dimension:
frame_height = frame_width / aspect_ratio
else:
frame_width = aspect_ratio * frame_height
self.frame.set_height(frame_height)
self.frame.set_width(frame_width)
self.frame.set_height(frame_height, stretch=true)
self.frame.set_width(frame_width, stretch=true)
# Rendering
def capture(self, *mobjects: Mobject) -> None:
@ -444,9 +445,10 @@ class Camera(object):
# Program and vertex array
shader_program, vert_format = self.get_shader_program(shader_wrapper)
attributes = shader_wrapper.vert_attributes
vao = self.ctx.vertex_array(
program=shader_program,
content=[(vbo, vert_format, *shader_wrapper.vert_attributes)],
content=[(vbo, vert_format, *attributes)],
index_buffer=ibo,
)
return {
@ -502,16 +504,14 @@ class Camera(object):
def refresh_perspective_uniforms(self) -> None:
frame = self.frame
# Orient light
perspective_transform = frame.get_perspective_transform()
view_matrix = frame.get_view_matrix()
light_pos = self.light_source.get_location()
cam_pos = self.frame.get_implied_camera_location()
frame_shape = frame.get_shape()
self.perspective_uniforms.update(
frame_shape=frame_shape,
pixel_size=frame_shape[0] / self.get_pixel_shape()[0],
perspective=tuple(perspective_transform.T.flatten()),
frame_shape=frame.get_shape(),
pixel_size=self.get_pixel_size(),
view=tuple(view_matrix.T.flatten()),
camera_position=tuple(cam_pos),
light_position=tuple(light_pos),
focal_distance=frame.get_focal_distance(),

View File

@ -414,9 +414,10 @@ def get_window_config(args: Namespace, custom_config: dict, camera_config: dict)
if not (args.full_screen or custom_config["full_screen"]):
window_width //= 2
window_height = int(window_width / aspect_ratio)
return {
"size": (window_width, window_height),
}
return dict(
full_size=(camera_config["pixel_width"], camera_config["pixel_height"]),
size=(window_width, window_height),
)
def get_camera_config(args: Namespace, custom_config: dict) -> dict:

View File

@ -27,16 +27,19 @@ from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import RED
from manimlib.constants import RIGHT
from manimlib.constants import RIGHT
from manimlib.constants import SMALL_BUFF
from manimlib.constants import SMALL_BUFF
from manimlib.constants import UP
from manimlib.constants import UP
from manimlib.constants import UL
from manimlib.constants import UR
from manimlib.constants import DL
from manimlib.constants import DR
from manimlib.constants import WHITE
from manimlib.constants import YELLOW
from manimlib.mobject.boolean_ops import Difference
from manimlib.mobject.geometry import Arc
from manimlib.mobject.geometry import Circle
from manimlib.mobject.geometry import Dot
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Polygon
from manimlib.mobject.geometry import Rectangle
@ -577,3 +580,50 @@ class Piano3D(VGroup):
if piano_2d[i] in piano_2d.black_keys:
key.shift(black_key_shift * OUT)
key.set_color(BLACK)
class DieFace(VGroup):
def __init__(
self,
value: int,
side_length: float = 1.0,
corner_radius: float = 0.15,
stroke_color: ManimColor = WHITE,
stroke_width: float = 2.0,
fill_color: ManimColor = GREY_E,
dot_radius: float = 0.08,
dot_color: ManimColor = BLUE_B,
dot_coalesce_factor: float = 0.5
):
dot = Dot(radius=dot_radius, fill_color=dot_color)
square = Square(
side_length=side_length,
stroke_color=stroke_color,
stroke_width=stroke_width,
fill_color=fill_color,
fill_opacity=1.0,
)
square.round_corners(corner_radius)
if not (1 <= value <= 6):
raise Exception("DieFace only accepts integer inputs between 1 and 6")
edge_group = [
(ORIGIN,),
(UL, DR),
(UL, ORIGIN, DR),
(UL, UR, DL, DR),
(UL, UR, ORIGIN, DL, DR),
(UL, UR, LEFT, RIGHT, DL, DR),
][value - 1]
arrangement = VGroup(*(
dot.copy().move_to(square.get_bounding_box_point(vect))
for vect in edge_group
))
arrangement.space_out_submobjects(dot_coalesce_factor)
super().__init__(square, arrangement)
self.value = value
self.index = value

View File

@ -68,8 +68,8 @@ class InteractiveScene(Scene):
"""
corner_dot_config = dict(
color=WHITE,
radius=0.05,
glow_factor=1.0,
radius=0.025,
glow_factor=2.0,
)
selection_rectangle_stroke_color = WHITE
selection_rectangle_stroke_width = 1.0

View File

@ -101,7 +101,7 @@ class Scene(object):
if self.preview:
from manimlib.window import Window
self.window = Window(scene=self, **self.window_config)
self.camera_config["ctx"] = self.window.ctx
self.camera_config["window"] = self.window
self.camera_config["fps"] = 30 # Where's that 30 from?
else:
self.window = None
@ -289,7 +289,15 @@ class Scene(object):
# Only these methods should touch the camera
def get_image(self) -> Image:
return self.camera.get_image()
if self.window is not None:
self.window.size = self.camera.get_pixel_shape()
self.window.swap_buffers()
self.update_frame()
self.window.swap_buffers()
image = self.camera.get_image()
if self.window is not None:
self.window.to_default_position()
return image
def show(self) -> None:
self.update_frame(ignore_skipping=True)
@ -711,7 +719,7 @@ class Scene(object):
self.restore_state(self.redo_stack.pop())
self.refresh_static_mobjects()
def checkpoint_paste(self, skip: bool = False):
def checkpoint_paste(self, skip: bool = False, record: bool = False):
"""
Used during interactive development to run (or re-run)
a block of scene code.
@ -721,7 +729,7 @@ class Scene(object):
was called on a block of code starting with that comment.
"""
shell = get_ipython()
if shell is None:
if shell is None or self.window is None:
raise Exception(
"Scene.checkpoint_paste cannot be called outside of " +
"an ipython shell"
@ -738,8 +746,20 @@ class Scene(object):
prev_skipping = self.skip_animations
self.skip_animations = skip
if record:
# Resize window so rendering happens at the appropriate size
self.window.size = self.camera.get_pixel_shape()
self.window.swap_buffers()
self.update_frame()
self.file_writer.begin_insert()
shell.run_cell(pasted)
if record:
self.file_writer.end_insert()
# Put window back to how it started
self.window.to_default_position()
self.skip_animations = prev_skipping
def checkpoint(self, key: str):
@ -901,7 +921,7 @@ class Scene(object):
self.hold_on_wait = False
def on_resize(self, width: int, height: int) -> None:
self.camera.reset_pixel_shape(width, height)
pass
def on_show(self) -> None:
pass

View File

@ -288,6 +288,25 @@ class SceneFileWriter(object):
)
self.set_progress_display_description()
def begin_insert(self):
# Begin writing process
self.write_to_movie = True
self.init_output_directories()
movie_path = self.get_movie_file_path()
folder, file = os.path.split(movie_path)
scene_name, ext = file.split(".")
n_inserts = len(list(filter(
lambda f: f.startswith(scene_name + "_insert"),
os.listdir(folder)
)))
self.inserted_file_path = movie_path.replace(".", f"_insert_{n_inserts}.")
self.open_movie_pipe(self.inserted_file_path)
def end_insert(self):
self.close_movie_pipe()
self.write_to_movie = False
self.print_file_ready_message(self.inserted_file_path)
def has_progress_display(self):
return self.progress_display is not None

View File

@ -1,18 +1,18 @@
uniform float is_fixed_in_frame;
uniform mat4 perspective;
uniform mat4 view;
uniform vec2 frame_shape;
uniform float focal_distance;
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
vec4 get_gl_Position(vec3 point){
bool is_fixed = bool(is_fixed_in_frame);
vec4 result = vec4(point, 1.0);
vec2 shape = DEFAULT_FRAME_SHAPE;
if(!bool(is_fixed_in_frame)){
result = perspective * result;
shape = frame_shape;
if(!is_fixed){
result = view * result;
}
vec2 shape = is_fixed ? DEFAULT_FRAME_SHAPE : frame_shape;
result.x *= 2.0 / shape.x;
result.y *= 2.0 / shape.y;
result.z /= focal_distance;

View File

@ -42,12 +42,8 @@ vec3 unit_normal = vec3(0.0, 0.0, 1.0);
vec3 get_joint_unit_normal(vec4 joint_product){
vec3 result;
if(joint_product.w < COS_THRESHOLD){
result = joint_product.xyz;
}else{
result = v_joint_product[1].xyz;
}
vec3 result = (joint_product.w < COS_THRESHOLD) ?
joint_product.xyz : v_joint_product[1].xyz;
float norm = length(result);
return (norm > 1e-5) ? result / norm : vec3(0.0, 0.0, 1.0);
}
@ -104,8 +100,8 @@ void get_corners(
float buff2 = 0.5 * v_stroke_width[2] + aaw;
// Add correction for sharp angles to prevent weird bevel effects
if(v_joint_product[0].w < -0.75) buff0 *= 4 * (v_joint_product[0].w + 1.0);
if(v_joint_product[2].w < -0.75) buff2 *= 4 * (v_joint_product[2].w + 1.0);
if(v_joint_product[0].w < -0.9) buff0 *= 10 * (v_joint_product[0].w + 1.0);
if(v_joint_product[2].w < -0.9) buff2 *= 10 * (v_joint_product[2].w + 1.0);
// Unit normal and joint angles
vec3 normal0 = get_joint_unit_normal(v_joint_product[0]);

View File

@ -26,10 +26,14 @@ class Window(PygletWindow):
self,
scene: Scene,
size: tuple[int, int] = (1280, 720),
full_size: tuple[int, int] = (1920, 1080),
samples = 0
):
super().__init__(size=size, samples=samples)
super().__init__(size=full_size, samples=samples)
self.full_size = full_size
self.default_size = size
self.default_position = self.find_initial_position(size)
self.scene = scene
self.pressed_keys = set()
self.title = str(scene)
@ -40,13 +44,12 @@ class Window(PygletWindow):
self.config = mglw.WindowConfig(ctx=self.ctx, wnd=self, timer=self.timer)
self.timer.start()
# No idea why, but when self.position is set once
# it sometimes doesn't actually change the position
# to the specified tuple on the rhs, but doing it
# twice seems to make it work. ¯\_(ツ)_/¯
initial_position = self.find_initial_position(size)
self.position = initial_position
self.position = initial_position
self.to_default_position()
def to_default_position(self):
self.size = self.default_size
self.position = self.default_position
self.swap_buffers()
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
custom_position = get_customization()["window_position"]