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,
}
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)

View file

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

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