Basic preview window

This commit is contained in:
Grant Sanderson 2020-02-11 19:51:19 -08:00
parent 2cf21fd0ad
commit 960e918e61
3 changed files with 233 additions and 95 deletions

View file

@ -26,11 +26,12 @@ class CameraFrame(Mobject):
"center": ORIGIN, "center": ORIGIN,
} }
def generate_points(self): def init_points(self):
self.points = np.array([UL, UR, DR, DL]) self.points = np.array([UL, UR, DR, DL])
self.set_width(self.width, stretch=True) self.set_width(self.width, stretch=True)
self.set_height(self.height, stretch=True) self.set_height(self.height, stretch=True)
self.move_to(self.center) self.move_to(self.center)
self.save_state()
class Camera(object): class Camera(object):
@ -43,7 +44,7 @@ class Camera(object):
}, },
"pixel_height": DEFAULT_PIXEL_HEIGHT, "pixel_height": DEFAULT_PIXEL_HEIGHT,
"pixel_width": DEFAULT_PIXEL_WIDTH, "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 # Note: frame height and width will be resized to match
# the pixel aspect ratio # the pixel aspect ratio
"background_color": BLACK, "background_color": BLACK,
@ -55,37 +56,37 @@ class Camera(object):
"n_channels": 4, "n_channels": 4,
"pixel_array_dtype": 'uint8', "pixel_array_dtype": 'uint8',
"line_width_multiple": 0.01, "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()) digest_config(self, kwargs, locals())
self.background_fbo = None
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
self.init_frame() self.init_frame()
self.init_context() self.init_context()
self.init_frame_buffer()
self.init_shaders() self.init_shaders()
def init_frame(self): def init_frame(self):
self.frame = CameraFrame(**self.frame_config) self.frame = CameraFrame(**self.frame_config)
def init_context(self): def init_context(self):
# TODO, context with a window? if self.window is not None:
ctx = moderngl.create_standalone_context() self.ctx = self.window.ctx
ctx.enable(moderngl.BLEND) self.fbo = self.window.ctx.detect_framebuffer()
ctx.blend_func = ( 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.SRC_ALPHA, moderngl.ONE_MINUS_SRC_ALPHA,
moderngl.ONE, moderngl.ONE moderngl.ONE, moderngl.ONE
) )
self.ctx = ctx
# Methods associated with the frame buffer # 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): def get_fbo(self):
return self.ctx.simple_framebuffer(self.get_pixel_shape()) return self.ctx.simple_framebuffer(self.get_pixel_shape())
@ -109,24 +110,28 @@ class Camera(object):
self.set_frame_width(frame_width) self.set_frame_width(frame_width)
def clear(self): def clear(self):
if self.background_fbo: if self.window:
self.ctx.copy_framebuffer(self.fbo, self.background_fbo) self.window.clear()
else:
rgba = (*Color(self.background_color).get_rgb(), self.background_opacity) rgba = (*Color(self.background_color).get_rgb(), self.background_opacity)
self.fbo.clear(*rgba) self.fbo.clear(*rgba)
def lock_state_as_background(self): def lock_state_as_background(self):
self.background_fbo = self.get_fbo() # TODO, somehow do this by creating a Texture
self.ctx.copy_framebuffer(self.background_fbo, self.fbo) # and adding it to the queue like an image mobject
pass
def unlock_background(self): def unlock_background(self):
pass # TODO
self.background_fbo = None 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_width = new_width
self.pixel_height = new_height self.pixel_height = new_height
self.fbo.release() self.fbo.release()
self.init_frame_buffer() self.init_frame_buffer()
self.refresh_shader_uniforms()
# Various ways to read from the fbo # Various ways to read from the fbo
def get_raw_fbo_data(self, dtype='f1'): def get_raw_fbo_data(self, dtype='f1'):
@ -173,6 +178,9 @@ class Camera(object):
def get_frame_width(self): def get_frame_width(self):
return self.frame.get_width() return self.frame.get_width()
def get_frame_shape(self):
return (self.get_frame_width(), self.get_frame_height())
def get_frame_center(self): def get_frame_center(self):
return self.frame.get_center() return self.frame.get_center()
@ -185,6 +193,21 @@ class Camera(object):
def set_frame_center(self, center): def set_frame_center(self, center):
self.frame.move_to(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 # TODO, account for 3d
def is_in_frame(self, mobject): def is_in_frame(self, mobject):
fc = self.get_frame_center() fc = self.get_frame_center()
@ -225,7 +248,10 @@ class Camera(object):
render_primative = info_group[0]["render_primative"] render_primative = info_group[0]["render_primative"]
self.render_from_shader(shader, data, render_primative) self.render_from_shader(shader, data, render_primative)
# Shader stuff if self.window:
self.window.swap_buffers()
# Shaders
def init_shaders(self): def init_shaders(self):
self.id_to_shader = {} self.id_to_shader = {}
@ -271,52 +297,23 @@ class Camera(object):
return result return result
def set_shader_uniforms(self, shader): def set_shader_uniforms(self, shader):
# TODO, think about how uniforms come from mobjects # TODO, think about how uniforms come from mobjects as well.
# as well.
fw = self.get_frame_width()
fh = self.get_frame_height() fh = self.get_frame_height()
fc = self.get_frame_center()
pw, ph = self.get_pixel_shape()
shader['scale'].value = fh / 2 shader['scale'].value = fh / 2 # Scale based on frame size
shader['aspect_ratio'].value = fw / fh shader['aspect_ratio'].value = (pw / ph) # AR based on pixel shape
shader['anti_alias_width'].value = ANTI_ALIAS_WIDTH 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): def render_from_shader(self, shader, data, render_primative):
vbo = shader.ctx.buffer(data.tobytes()) if len(data) == 0:
vao = shader.ctx.simple_vertex_array(shader, vbo, *data.dtype.names) return
vbo = self.ctx.buffer(data.tobytes())
vao = self.ctx.simple_vertex_array(shader, vbo, *data.dtype.names)
vao.render(render_primative) 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)

View file

@ -5,6 +5,7 @@ import platform
from tqdm import tqdm as ProgressDisplay from tqdm import tqdm as ProgressDisplay
import numpy as np import numpy as np
import time
from manimlib.animation.animation import Animation from manimlib.animation.animation import Animation
from manimlib.animation.transform import MoveToTarget 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.scene.scene_file_writer import SceneFileWriter
from manimlib.utils.family_ops import extract_mobject_family_members 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.utils.family_ops import restructure_list_to_exclude_certain_family_members
from manimlib.window import Window
class Scene(Container): class Scene(Container):
CONFIG = { CONFIG = {
"camera_class": Camera, # TODO, there should be only one camera "window_config": {},
"camera_class": Camera,
"camera_config": {}, "camera_config": {},
"file_writer_config": {}, "file_writer_config": {},
"skip_animations": False, "skip_animations": False,
@ -28,29 +31,37 @@ class Scene(Container):
"start_at_animation_number": None, "start_at_animation_number": None,
"end_at_animation_number": None, "end_at_animation_number": None,
"leave_progress_bars": False, "leave_progress_bars": False,
"preview": True,
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
Container.__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.camera = self.camera_class(**self.camera_config)
self.file_writer = SceneFileWriter(self, **self.file_writer_config) self.file_writer = SceneFileWriter(self, **self.file_writer_config)
self.mobjects = [] self.mobjects = []
self.num_plays = 0 self.num_plays = 0
self.time = 0 self.time = 0
self.original_skipping_status = self.skip_animations self.original_skipping_status = self.skip_animations
self.time_of_last_frame = time.time()
if self.random_seed is not None: if self.random_seed is not None:
random.seed(self.random_seed) random.seed(self.random_seed)
np.random.seed(self.random_seed) np.random.seed(self.random_seed)
def run(self):
self.setup() self.setup()
try: try:
self.construct() self.construct()
except EndSceneEarlyException: except EndSceneEarlyException:
pass pass
self.tear_down() 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): def setup(self):
""" """
@ -61,10 +72,20 @@ class Scene(Container):
pass pass
def construct(self): def construct(self):
pass # To be implemented in subclasses # To be implemented in subclasses
pass
def tear_down(self): 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): def __str__(self):
return self.__class__.__name__ return self.__class__.__name__
@ -91,9 +112,6 @@ class Scene(Container):
return [getattr(self, key) for key in keys] return [getattr(self, key) for key in keys]
# Only these methods should touch the camera # Only these methods should touch the camera
def set_camera(self, camera):
self.camera = camera
def get_image(self): def get_image(self):
return self.camera.get_image() return self.camera.get_image()
@ -103,15 +121,23 @@ class Scene(Container):
if mobjects is None: if mobjects is None:
mobjects = self.mobjects mobjects = self.mobjects
## REMOVE, this is just temporary while camera.lock_background doesn't work
mobjects = self.mobjects
##
self.camera.clear() self.camera.clear()
self.camera.capture_mobjects(mobjects, excluded_mobjects=excluded_mobjects) self.camera.capture_mobjects(mobjects, excluded_mobjects=excluded_mobjects)
def write_frame(self): def emit_frame(self, dt):
self.increment_time(1.0 / self.camera.frame_rate) self.increment_time(dt)
if self.skip_animations: if not self.skip_animations:
return self.file_writer.write_frame(self.camera)
data = self.camera.get_raw_fbo_data()
self.file_writer.write_frame(data) 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 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 Each arg can either be an animation, or a mobject method
followed by that methods arguments (and potentially follow followed by that methods arguments (and potentially follow
@ -314,13 +340,16 @@ class Scene(Container):
self.skip_animations = True self.skip_animations = True
raise EndSceneEarlyException() raise EndSceneEarlyException()
# Methods associated with running animations
def handle_play_like_call(func): def handle_play_like_call(func):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
self.update_skipping_status() self.update_skipping_status()
allow_write = not self.skip_animations 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) func(self, *args, **kwargs)
self.file_writer.end_animation(allow_write) if allow_write:
self.file_writer.end_animation()
self.num_plays += 1 self.num_plays += 1
return wrapper return wrapper
@ -352,7 +381,7 @@ class Scene(Container):
animation.interpolate(alpha) animation.interpolate(alpha)
self.update_mobjects(dt) self.update_mobjects(dt)
self.update_frame(moving_mobjects) self.update_frame(moving_mobjects)
self.write_frame() self.emit_frame(dt)
self.camera.unlock_background() self.camera.unlock_background()
def finish_animations(self, animations): def finish_animations(self, animations):
@ -373,16 +402,11 @@ class Scene(Container):
if len(args) == 0: if len(args) == 0:
warnings.warn("Called Scene.play with no animations") warnings.warn("Called Scene.play with no animations")
return return
animations = self.compile_play_args_to_animation_list( animations = self.anims_from_play_args(*args, **kwargs)
*args, **kwargs
)
self.begin_animations(animations) self.begin_animations(animations)
self.progress_through_animations(animations) self.progress_through_animations(animations)
self.finish_animations(animations) self.finish_animations(animations)
def idle_stream(self):
self.file_writer.idle_stream()
def clean_up_animations(self, *animations): def clean_up_animations(self, *animations):
for animation in animations: for animation in animations:
animation.clean_up_from_scene(self) animation.clean_up_from_scene(self)
@ -423,7 +447,7 @@ class Scene(Container):
last_t = t last_t = t
self.update_mobjects(dt) self.update_mobjects(dt)
self.update_frame() self.update_frame()
self.write_frame() self.emit_frame(dt)
if stop_condition is not None and stop_condition(): if stop_condition is not None and stop_condition():
time_progression.close() time_progression.close()
break break
@ -435,7 +459,7 @@ class Scene(Container):
dt = 1 / self.camera.frame_rate dt = 1 / self.camera.frame_rate
n_frames = int(duration / dt) n_frames = int(duration / dt)
for n in range(n_frames): for n in range(n_frames):
self.write_frame() self.emit_frame(dt)
return self return self
def wait_until(self, stop_condition, max_time=60): def wait_until(self, stop_condition, max_time=60):
@ -461,6 +485,44 @@ class Scene(Container):
self.update_frame(ignore_skipping=True) self.update_frame(ignore_skipping=True)
self.get_image().show() 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): class EndSceneEarlyException(Exception):
pass pass

79
manimlib/window.py Normal file
View file

@ -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()