Change the way changing-vs-static mobjects are tracked

Previously, Camera would keep track of which mobjects are supposed to be "static", so that it could generated their render groups once and not repeat unnecessarily. This had an awkward dependence where Scene would then need to keep track of which mobjects should and should not be considered static.

This update pushes that logic to the Mobject level, where it keeps track internally of whether it's being animated, has an updater, or can be moved around by the mouse.
This commit is contained in:
Grant Sanderson 2022-04-14 16:27:58 -07:00
parent 5a91c73b23
commit 50565fcd7a
4 changed files with 55 additions and 47 deletions

View file

@ -52,6 +52,7 @@ class Animation(object):
# played. As much initialization as possible,
# especially any mobject copying, should live in
# this method
self.mobject.set_animating_status(True)
self.starting_mobject = self.create_starting_mobject()
if self.suspend_mobject_updating:
# All calls to self.mobject's internal updaters
@ -66,6 +67,7 @@ class Animation(object):
def finish(self) -> None:
self.interpolate(self.final_alpha_value)
self.mobject.set_animating_status(False)
if self.suspend_mobject_updating:
self.mobject.resume_updating()

View file

@ -199,7 +199,10 @@ class Camera(object):
self.init_textures()
self.init_light_source()
self.refresh_perspective_uniforms()
self.static_mobject_to_render_group_list = {}
# A cached map from mobjects to their associated list of render groups
# so that these render groups are not regenerated unnecessarily for static
# mobjects
self.mob_to_render_groups = {}
def init_frame(self) -> None:
self.frame = CameraFrame(**self.frame_config)
@ -365,11 +368,21 @@ class Camera(object):
if render_group["single_use"]:
self.release_render_group(render_group)
def get_render_group_list(self, mobject: Mobject) -> list[dict[str]] | map[dict[str]]:
try:
return self.static_mobject_to_render_group_list[id(mobject)]
except KeyError:
return map(self.get_render_group, mobject.get_shader_wrapper_list())
def get_render_group_list(self, mobject: Mobject) -> Iterable[dict[str]]:
if mobject.is_changing():
return self.generate_render_group_list(mobject)
# Otherwise, cache result for later use
key = id(mobject)
if key not in self.mob_to_render_groups:
self.mob_to_render_groups[key] = list(self.generate_render_group_list(mobject))
return self.mob_to_render_groups[key]
def generate_render_group_list(self, mobject: Mobject) -> Iterable[dict[str]]:
return (
self.get_render_group(sw, single_use=mobject.is_changing())
for sw in mobject.get_shader_wrapper_list()
)
def get_render_group(
self,
@ -408,19 +421,10 @@ class Camera(object):
if render_group[key] is not None:
render_group[key].release()
def set_mobjects_as_static(self, *mobjects: Mobject) -> None:
# Creates buffer and array objects holding each mobjects shader data
for mob in mobjects:
self.static_mobject_to_render_group_list[id(mob)] = [
self.get_render_group(sw, single_use=False)
for sw in mob.get_shader_wrapper_list()
]
def release_static_mobjects(self) -> None:
for rg_list in self.static_mobject_to_render_group_list.values():
for render_group in rg_list:
self.release_render_group(render_group)
self.static_mobject_to_render_group_list = {}
def refresh_static_mobjects(self) -> None:
for render_group in it.chain(*self.mob_to_render_groups.values()):
self.release_render_group(render_group)
self.mob_to_render_groups = {}
# Shaders
def init_shaders(self) -> None:

View file

@ -71,7 +71,7 @@ class Mobject(object):
# Must match in attributes of vert shader
"shader_dtype": [
('point', np.float32, (3,)),
]
],
}
def __init__(self, **kwargs):
@ -81,6 +81,8 @@ class Mobject(object):
self.family: list[Mobject] = [self]
self.locked_data_keys: set[str] = set()
self.needs_new_bounding_box: bool = True
self._is_animating: bool = False
self._is_movable: bool = False
self.init_data()
self.init_uniforms()
@ -421,7 +423,6 @@ class Mobject(object):
self.center()
return self
def sort(
self,
point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0],
@ -637,6 +638,23 @@ class Mobject(object):
self.has_updaters = any(mob.get_updaters() for mob in self.get_family())
return self
# Check if mark as static or not for camera
def is_changing(self) -> bool:
return self._is_animating or self.has_updaters or self._is_movable
def set_animating_status(self, is_animating: bool) -> None:
self._is_animating = is_animating
def set_movable_status(self, is_movable: bool) -> None:
self._is_movable = is_movable
def is_movable(self) -> bool:
return self._is_movable
def make_movable(self) -> None:
self._is_movable = True
# Transforming operations
def shift(self, vector: np.ndarray):

View file

@ -115,16 +115,16 @@ class Scene(object):
# If there is a window, enter a loop
# which updates the frame while under
# the hood calling the pyglet event loop
log.info("Tips: You are now in the interactive mode. Now you can use the keyboard"
" and the mouse to interact with the scene. Just press `q` if you want to quit.")
log.info(
"Tips: You are now in the interactive mode. Now you can use the keyboard"
" and the mouse to interact with the scene. Just press `q` if you want to quit."
)
self.quit_interaction = False
self.lock_static_mobject_data()
self.refresh_static_mobjects()
while not (self.window.is_closing or self.quit_interaction):
self.update_frame(1 / self.camera.frame_rate)
if self.window.is_closing:
self.window.destroy()
if self.quit_interaction:
self.unlock_mobject_data()
def embed(self, close_scene_on_exit: bool = True) -> None:
if not self.preview:
@ -141,6 +141,7 @@ class Scene(object):
from IPython.terminal.embed import InteractiveShellEmbed
shell = InteractiveShellEmbed()
# Have the frame update after each command
shell.events.register('post_run_cell', lambda *a, **kw: self.refresh_static_mobjects())
shell.events.register('post_run_cell', lambda *a, **kw: self.update_frame())
# Use the locals of the caller as the local namespace
# once embedded, and add a few custom shortcuts
@ -442,6 +443,7 @@ class Scene(object):
self.real_animation_start_time = time.time()
self.virtual_animation_start_time = self.time
self.refresh_static_mobjects()
func(self, *args, **kwargs)
if should_write:
@ -450,23 +452,8 @@ class Scene(object):
self.num_plays += 1
return wrapper
def lock_static_mobject_data(self, *animations: Animation) -> None:
movers = list(it.chain(*[
anim.mobject.get_family()
for anim in animations
]))
for mobject in self.mobjects:
if mobject in movers or mobject.get_family_updaters():
continue
self.camera.set_mobjects_as_static(mobject)
def unlock_mobject_data(self) -> None:
self.camera.release_static_mobjects()
def refresh_locked_data(self):
self.unlock_mobject_data()
self.lock_static_mobject_data()
return self
def refresh_static_mobjects(self) -> None:
self.camera.refresh_static_mobjects()
def begin_animations(self, animations: Iterable[Animation]) -> None:
for animation in animations:
@ -506,11 +493,9 @@ class Scene(object):
log.warning("Called Scene.play with no animations")
return
animations = self.anims_from_play_args(*args, **kwargs)
self.lock_static_mobject_data(*animations)
self.begin_animations(animations)
self.progress_through_animations(animations)
self.finish_animations(animations)
self.unlock_mobject_data()
@handle_play_like_call
def wait(
@ -523,7 +508,6 @@ class Scene(object):
if note:
log.info(note)
self.update_mobjects(dt=0) # Any problems with this?
self.lock_static_mobject_data()
if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode:
while self.hold_on_wait:
self.update_frame(dt=1 / self.camera.frame_rate)
@ -538,7 +522,7 @@ class Scene(object):
self.emit_frame()
if stop_condition is not None and stop_condition():
break
self.unlock_mobject_data()
self.refresh_static_mobjects()
return self
def wait_until(