diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 0ebd0087..606b1126 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -26,11 +26,12 @@ class CameraFrame(Mobject): "center": ORIGIN, } - def generate_points(self): + def init_points(self): self.points = np.array([UL, UR, DR, DL]) self.set_width(self.width, stretch=True) self.set_height(self.height, stretch=True) self.move_to(self.center) + self.save_state() class Camera(object): @@ -43,7 +44,7 @@ class Camera(object): }, "pixel_height": DEFAULT_PIXEL_HEIGHT, "pixel_width": DEFAULT_PIXEL_WIDTH, - "frame_rate": DEFAULT_FRAME_RATE, + "frame_rate": DEFAULT_FRAME_RATE, # TODO, move this elsewhere # Note: frame height and width will be resized to match # the pixel aspect ratio "background_color": BLACK, @@ -55,37 +56,37 @@ class Camera(object): "n_channels": 4, "pixel_array_dtype": 'uint8', "line_width_multiple": 0.01, - "background_fbo": None, + "window": None, } - def __init__(self, background=None, **kwargs): + def __init__(self, **kwargs): digest_config(self, kwargs, locals()) + self.background_fbo = None self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max self.init_frame() self.init_context() - self.init_frame_buffer() self.init_shaders() def init_frame(self): self.frame = CameraFrame(**self.frame_config) def init_context(self): - # TODO, context with a window? - ctx = moderngl.create_standalone_context() - ctx.enable(moderngl.BLEND) - ctx.blend_func = ( + if self.window is not None: + self.ctx = self.window.ctx + self.fbo = self.window.ctx.detect_framebuffer() + else: + self.ctx = moderngl.create_standalone_context() + self.fbo = self.get_fbo() + self.fbo.use() + # self.clear() + + self.ctx.enable(moderngl.BLEND) + self.ctx.blend_func = ( moderngl.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA, moderngl.ONE, moderngl.ONE ) - self.ctx = ctx # Methods associated with the frame buffer - def init_frame_buffer(self): - # TODO, account for live window - self.fbo = self.get_fbo() - self.fbo.use() - self.clear() - def get_fbo(self): return self.ctx.simple_framebuffer(self.get_pixel_shape()) @@ -109,24 +110,28 @@ class Camera(object): self.set_frame_width(frame_width) def clear(self): - if self.background_fbo: - self.ctx.copy_framebuffer(self.fbo, self.background_fbo) - else: - rgba = (*Color(self.background_color).get_rgb(), self.background_opacity) - self.fbo.clear(*rgba) + if self.window: + self.window.clear() + + rgba = (*Color(self.background_color).get_rgb(), self.background_opacity) + self.fbo.clear(*rgba) def lock_state_as_background(self): - self.background_fbo = self.get_fbo() - self.ctx.copy_framebuffer(self.background_fbo, self.fbo) + # TODO, somehow do this by creating a Texture + # and adding it to the queue like an image mobject + pass def unlock_background(self): + pass # TODO self.background_fbo = None - def reset_pixel_shape(self, new_height, new_width): + def reset_pixel_shape(self, new_width, new_height): self.pixel_width = new_width self.pixel_height = new_height + self.fbo.release() self.init_frame_buffer() + self.refresh_shader_uniforms() # Various ways to read from the fbo def get_raw_fbo_data(self, dtype='f1'): @@ -173,6 +178,9 @@ class Camera(object): def get_frame_width(self): return self.frame.get_width() + def get_frame_shape(self): + return (self.get_frame_width(), self.get_frame_height()) + def get_frame_center(self): return self.frame.get_center() @@ -185,6 +193,21 @@ class Camera(object): def set_frame_center(self, center): self.frame.move_to(center) + def pixel_coords_to_space_coords(self, px, py, relative=False): + pw, ph = self.get_pixel_shape() + fw, fh = self.get_frame_shape() + fc = self.get_frame_center() + if relative: + return 2 * np.array([px / pw, py / ph, 0]) + else: + # Only scale wrt one axis + scale = fh / ph + return np.array([ + scale * (px - pw / 2) - fc[0], + scale * (py - py / 2) - fc[0], + -fc[2] / 2, + ]) + # TODO, account for 3d def is_in_frame(self, mobject): fc = self.get_frame_center() @@ -225,7 +248,10 @@ class Camera(object): render_primative = info_group[0]["render_primative"] self.render_from_shader(shader, data, render_primative) - # Shader stuff + if self.window: + self.window.swap_buffers() + + # Shaders def init_shaders(self): self.id_to_shader = {} @@ -271,52 +297,23 @@ class Camera(object): return result def set_shader_uniforms(self, shader): - # TODO, think about how uniforms come from mobjects - # as well. - fw = self.get_frame_width() + # TODO, think about how uniforms come from mobjects as well. fh = self.get_frame_height() + fc = self.get_frame_center() + pw, ph = self.get_pixel_shape() - shader['scale'].value = fh / 2 - shader['aspect_ratio'].value = fw / fh + shader['scale'].value = fh / 2 # Scale based on frame size + shader['aspect_ratio'].value = (pw / ph) # AR based on pixel shape shader['anti_alias_width'].value = ANTI_ALIAS_WIDTH + shader['frame_center'].value = tuple(fc) + + def refresh_shader_uniforms(self): + for sid, shader in self.id_to_shader.items(): + self.set_shader_uniforms(shader) def render_from_shader(self, shader, data, render_primative): - vbo = shader.ctx.buffer(data.tobytes()) - vao = shader.ctx.simple_vertex_array(shader, vbo, *data.dtype.names) + if len(data) == 0: + return + vbo = self.ctx.buffer(data.tobytes()) + vao = self.ctx.simple_vertex_array(shader, vbo, *data.dtype.names) vao.render(render_primative) - - -def get_vmob_shader(ctx, type): - vert_file = f"quadratic_bezier_{type}_vert.glsl" - geom_file = f"quadratic_bezier_{type}_geom.glsl" - frag_file = f"quadratic_bezier_{type}_frag.glsl" - - shader = ctx.program( - vertex_shader=get_code_from_file(vert_file), - geometry_shader=get_code_from_file(geom_file), - fragment_shader=get_code_from_file(frag_file), - ) - set_shader_uniforms(shader) - return shader - - -def get_stroke_shader(ctx): - return get_vmob_shader(ctx, "stroke") - - -def get_fill_shader(ctx): - return get_vmob_shader(ctx, "fill") - - -def render_vmob_stroke(shader, vmobs): - assert(len(vmobs) > 0) - data_arrays = [vmob.get_stroke_shader_data() for vmob in vmobs] - data = join_arrays(*data_arrays) - send_data_to_shader(shader, data) - - -def render_vmob_fill(shader, vmobs): - assert(len(vmobs) > 0) - data_arrays = [vmob.get_fill_shader_data() for vmob in vmobs] - data = join_arrays(*data_arrays) - send_data_to_shader(shader, data) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 0b3156d6..9a5f22b4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -5,6 +5,7 @@ import platform from tqdm import tqdm as ProgressDisplay import numpy as np +import time from manimlib.animation.animation import Animation from manimlib.animation.transform import MoveToTarget @@ -15,11 +16,13 @@ from manimlib.mobject.mobject import Mobject from manimlib.scene.scene_file_writer import SceneFileWriter from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.family_ops import restructure_list_to_exclude_certain_family_members +from manimlib.window import Window class Scene(Container): CONFIG = { - "camera_class": Camera, # TODO, there should be only one camera + "window_config": {}, + "camera_class": Camera, "camera_config": {}, "file_writer_config": {}, "skip_animations": False, @@ -28,29 +31,37 @@ class Scene(Container): "start_at_animation_number": None, "end_at_animation_number": None, "leave_progress_bars": False, + "preview": True, } def __init__(self, **kwargs): Container.__init__(self, **kwargs) + if self.preview: + self.window = Window(self, **self.window_config) + self.camera_config["window"] = self.window + self.camera = self.camera_class(**self.camera_config) self.file_writer = SceneFileWriter(self, **self.file_writer_config) - self.mobjects = [] self.num_plays = 0 self.time = 0 self.original_skipping_status = self.skip_animations + self.time_of_last_frame = time.time() if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) + def run(self): self.setup() try: self.construct() except EndSceneEarlyException: pass self.tear_down() - self.file_writer.finish() - self.print_end_message() + + # Is this what we want? + if self.preview: + self.update_until_closed() def setup(self): """ @@ -61,10 +72,20 @@ class Scene(Container): pass def construct(self): - pass # To be implemented in subclasses + # To be implemented in subclasses + pass def tear_down(self): - pass + self.file_writer.finish() + self.print_end_message() + + def update_until_closed(self): + while not self.window.is_closing: + now = time.time() + self.update_frame() + time.sleep( + max(1 / 30 - (time.time() - now), 0) + ) def __str__(self): return self.__class__.__name__ @@ -91,9 +112,6 @@ class Scene(Container): return [getattr(self, key) for key in keys] # Only these methods should touch the camera - def set_camera(self, camera): - self.camera = camera - def get_image(self): return self.camera.get_image() @@ -103,15 +121,23 @@ class Scene(Container): if mobjects is None: mobjects = self.mobjects + ## REMOVE, this is just temporary while camera.lock_background doesn't work + mobjects = self.mobjects + ## + self.camera.clear() self.camera.capture_mobjects(mobjects, excluded_mobjects=excluded_mobjects) - def write_frame(self): - self.increment_time(1.0 / self.camera.frame_rate) - if self.skip_animations: - return - data = self.camera.get_raw_fbo_data() - self.file_writer.write_frame(data) + def emit_frame(self, dt): + self.increment_time(dt) + if not self.skip_animations: + self.file_writer.write_frame(self.camera) + + if self.preview: + min_time_between_frames = 1 / self.camera.frame_rate + time_since_last = time.time() - self.time_of_last_frame + time.sleep(max(0, min_time_between_frames - time_since_last)) + self.time_of_last_frame = time.time() ### @@ -238,7 +264,7 @@ class Scene(Container): ])) return time_progression - def compile_play_args_to_animation_list(self, *args, **kwargs): + def anims_from_play_args(self, *args, **kwargs): """ Each arg can either be an animation, or a mobject method followed by that methods arguments (and potentially follow @@ -314,13 +340,16 @@ class Scene(Container): self.skip_animations = True raise EndSceneEarlyException() + # Methods associated with running animations def handle_play_like_call(func): def wrapper(self, *args, **kwargs): self.update_skipping_status() allow_write = not self.skip_animations - self.file_writer.begin_animation(allow_write) + if allow_write: + self.file_writer.begin_animation() func(self, *args, **kwargs) - self.file_writer.end_animation(allow_write) + if allow_write: + self.file_writer.end_animation() self.num_plays += 1 return wrapper @@ -352,7 +381,7 @@ class Scene(Container): animation.interpolate(alpha) self.update_mobjects(dt) self.update_frame(moving_mobjects) - self.write_frame() + self.emit_frame(dt) self.camera.unlock_background() def finish_animations(self, animations): @@ -373,16 +402,11 @@ class Scene(Container): if len(args) == 0: warnings.warn("Called Scene.play with no animations") return - animations = self.compile_play_args_to_animation_list( - *args, **kwargs - ) + animations = self.anims_from_play_args(*args, **kwargs) self.begin_animations(animations) self.progress_through_animations(animations) self.finish_animations(animations) - def idle_stream(self): - self.file_writer.idle_stream() - def clean_up_animations(self, *animations): for animation in animations: animation.clean_up_from_scene(self) @@ -423,7 +447,7 @@ class Scene(Container): last_t = t self.update_mobjects(dt) self.update_frame() - self.write_frame() + self.emit_frame(dt) if stop_condition is not None and stop_condition(): time_progression.close() break @@ -435,7 +459,7 @@ class Scene(Container): dt = 1 / self.camera.frame_rate n_frames = int(duration / dt) for n in range(n_frames): - self.write_frame() + self.emit_frame(dt) return self def wait_until(self, stop_condition, max_time=60): @@ -461,6 +485,44 @@ class Scene(Container): self.update_frame(ignore_skipping=True) self.get_image().show() + # Event handling + def on_mouse_motion(self, point, d_point): + pass + + def on_mouse_drag(self, point, d_point, buttons, modifiers): + self.camera.frame.shift(-d_point) + self.camera.refresh_shader_uniforms() + + def on_mouse_press(self, point, button, mods): + pass + + def on_mouse_release(self, point, button, mods): + pass + + def on_mouse_scroll(self, point, offset): + self.camera.frame.scale(1 + np.arctan(offset[1])) + self.camera.refresh_shader_uniforms() + + def on_key_release(self, symbol, modifiers): + pass + + def on_key_press(self, symbol, modifiers): + if chr(symbol) == "r": + self.camera.frame.restore() + self.camera.refresh_shader_uniforms() + + def on_resize(self, width: int, height: int): + self.camera.reset_pixel_shape(width, height) + + def on_show(self): + pass + + def on_hide(self): + pass + + def on_close(self): + pass + class EndSceneEarlyException(Exception): pass diff --git a/manimlib/window.py b/manimlib/window.py new file mode 100644 index 00000000..fcb4b227 --- /dev/null +++ b/manimlib/window.py @@ -0,0 +1,79 @@ +import moderngl_window as mglw +from moderngl_window.context.pyglet.window import Window as PygletWindow + +from manimlib.constants import DEFAULT_PIXEL_WIDTH +from manimlib.constants import DEFAULT_PIXEL_HEIGHT + + +class Window(PygletWindow): + title = "" + size = (DEFAULT_PIXEL_WIDTH, DEFAULT_PIXEL_HEIGHT) + fullscreen = False + resizable = True + gl_version = (3, 3) + aspect_ratio = None + vsync = True + samples = 1 + cursor = True + + def __init__(self, scene, **kwargs): + super().__init__(**kwargs) + self.scene = scene + # self.print_context_info() + mglw.activate_context(window=self) + + # Delegate event handling to scene + def pixel_coords_to_space_coords(self, px, py, relative=False): + return self.scene.camera.pixel_coords_to_space_coords(px, py, relative) + + def on_mouse_motion(self, x, y, dx, dy): + super().on_mouse_motion(x, y, dx, dy) + point = self.pixel_coords_to_space_coords(x, y) + d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) + self.scene.on_mouse_motion(point, d_point) + + def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + super().on_mouse_drag(x, y, dx, dy, buttons, modifiers) + point = self.pixel_coords_to_space_coords(x, y) + d_point = self.pixel_coords_to_space_coords(dx, dy, relative=True) + self.scene.on_mouse_drag(point, d_point, buttons, modifiers) # Do a conversion? + + def on_mouse_press(self, x: int, y: int, button, mods): + super().on_mouse_press(x, y, button, mods) + point = self.pixel_coords_to_space_coords(x, y) + self.scene.on_mouse_press(point, button, mods) + + def on_mouse_release(self, x: int, y: int, button, mods): + super().on_mouse_release(x, y, button, mods) + point = self.pixel_coords_to_space_coords(x, y) + self.scene.on_mouse_release(point, button, mods) + + def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float): + super().on_mouse_scroll(x, y, x_offset, y_offset) + point = self.pixel_coords_to_space_coords(x, y) + offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True) + self.scene.on_mouse_scroll(point, offset) + + def on_key_release(self, symbol, modifiers): + super().on_key_release(symbol, modifiers) + self.scene.on_key_release(symbol, modifiers) + + def on_key_press(self, symbol, modifiers): + super().on_key_press(symbol, modifiers) + self.scene.on_key_press(symbol, modifiers) + + def on_resize(self, width: int, height: int): + super().on_resize(width, height) + self.scene.on_resize(width, height) + + def on_show(self): + super().on_show() + self.scene.on_show() + + def on_hide(self): + super().on_hide() + self.scene.on_hide() + + def on_close(self): + super().on_close() + self.scene.on_close()