mirror of
https://github.com/3b1b/manim.git
synced 2025-07-30 05:24:22 +08:00
@ -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(),
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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]);
|
||||
|
@ -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"]
|
||||
|
Reference in New Issue
Block a user