mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Merge branch 'experimental' of github.com:ManimCommunity/manim into experimental
This commit is contained in:
commit
e550c5586c
12 changed files with 400 additions and 559 deletions
|
|
@ -22,18 +22,6 @@ from manim.mobject.text.numbers import DecimalNumber
|
|||
from manim.mobject.text.text_mobject import Text
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
|
||||
|
||||
def progress_through_animations(animations):
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
for animation in animations:
|
||||
animation.update_mobjects(dt)
|
||||
alpha = t / animation.run_time
|
||||
animation.interpolate(alpha)
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with tempconfig({"renderer": "opengl"}):
|
||||
win = Window(
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ class Test(Scene):
|
|||
|
||||
|
||||
with tempconfig({"renderer": "opengl", "preview": True, "parallel": False}):
|
||||
Test().render()
|
||||
Manager(Test).render()
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ from .mobject.types.point_cloud_mobject import *
|
|||
from .mobject.types.vectorized_mobject import *
|
||||
from .mobject.value_tracker import *
|
||||
from .mobject.vector_field import *
|
||||
from .renderer.render_manager import *
|
||||
from .scene.scene import *
|
||||
from .scene.scene_file_writer import *
|
||||
from .scene.section import *
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ render_options = option_group(
|
|||
case_sensitive=False,
|
||||
),
|
||||
help="Select a renderer for your Scene.",
|
||||
default="cairo",
|
||||
default="opengl",
|
||||
),
|
||||
option(
|
||||
"-g",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import numpy as np
|
|||
from manim.utils.hashing import get_hash_from_play_call
|
||||
|
||||
from .. import config, logger
|
||||
from ..camera.cairo_camera import CairoCamera as Camera
|
||||
from ..mobject.mobject import Mobject
|
||||
from ..scene.scene_file_writer import SceneFileWriter
|
||||
from ..utils.exceptions import EndSceneEarlyException
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from manim.camera.camera import Camera
|
|||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.renderer.buffers.buffer import STD140BufferFormat
|
||||
from manim.renderer.opengl_shader_program import load_shader_program_by_folder
|
||||
from manim.renderer.renderer import ImageType, Renderer, RendererData
|
||||
from manim.renderer.renderer import ImageType, Renderer, RendererData, RendererProtocol
|
||||
from manim.utils.iterables import listify
|
||||
from manim.utils.space_ops import cross2d, earclip_triangulation, z_to_vector
|
||||
|
||||
|
|
@ -203,14 +203,14 @@ class ProgramManager:
|
|||
uniform_buffer_object.bind_to_uniform_block(idx)
|
||||
|
||||
|
||||
class OpenGLRenderer(Renderer):
|
||||
class OpenGLRenderer(Renderer, RendererProtocol):
|
||||
pixel_array_dtype = np.uint8
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pixel_width: int = config.pixel_width,
|
||||
pixel_height: int = config.pixel_height,
|
||||
samples=4,
|
||||
samples: int = 4,
|
||||
background_color: c.ManimColor = color.BLACK,
|
||||
background_opacity: float = 1.0,
|
||||
background_image: str | None = None,
|
||||
|
|
@ -407,6 +407,12 @@ class OpenGLRenderer(Renderer):
|
|||
vao.release()
|
||||
# return data, data_size
|
||||
|
||||
def render_image(self, mob):
|
||||
raise NotImplementedError # TODO
|
||||
|
||||
def render_previous(self, camera: Camera) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def render_vmobject(self, mob: OpenGLVMobject) -> None: # type: ignore
|
||||
self.stencil_buffer_fbo.use()
|
||||
self.stencil_buffer_fbo.clear()
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ from screeninfo import get_monitors
|
|||
|
||||
from .. import __version__, config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import manim.scene as m_scene
|
||||
|
||||
|
||||
class Window(FunWindow):
|
||||
fullscreen: bool = False
|
||||
|
|
|
|||
|
|
@ -85,9 +85,8 @@ def load_shader_program_by_folder(ctx: gl.Context, folder_name: str):
|
|||
raise RuntimeError("Loading Shader Program Error")
|
||||
if geometry_code is None:
|
||||
return ctx.program(vertex_shader=vertex_code, fragment_shader=fragment_code)
|
||||
elif geometry_code is not None:
|
||||
return ctx.program(
|
||||
vertex_shader=vertex_code,
|
||||
geometry_shader=geometry_code,
|
||||
fragment_shader=fragment_code,
|
||||
)
|
||||
return ctx.program(
|
||||
vertex_shader=vertex_code,
|
||||
geometry_shader=geometry_code,
|
||||
fragment_shader=fragment_code,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,82 +1,247 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import multiprocessing as mp
|
||||
import queue # NOTE: Cannot use mp.Queue because of auth keys
|
||||
from typing import TYPE_CHECKING, Any, Iterable
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterable
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim import config, logger
|
||||
from manim.constants import RendererType
|
||||
from manim.renderer.cairo_renderer import CairoRenderer
|
||||
from manim.utils.exceptions import EndSceneEarlyException
|
||||
|
||||
from ..scene.scene import Scene, SceneState
|
||||
from .opengl_file_writer import FileWriter
|
||||
from .opengl_renderer import OpenGLRenderer
|
||||
from .opengl_renderer_window import Window
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
|
||||
from ..camera.camera import Camera
|
||||
from ..scene.scene import SceneState
|
||||
from .renderer import RendererProtocol
|
||||
|
||||
__all__ = ("RenderManager",)
|
||||
__all__ = ("Manager",)
|
||||
|
||||
|
||||
class RenderManager:
|
||||
class Manager:
|
||||
"""
|
||||
Manage rendering in parallel
|
||||
The Brain of Manim
|
||||
|
||||
.. note::
|
||||
|
||||
The only method of this class officially guaranteed to be
|
||||
stable is :meth:`~.Manager.render`. Any other methods documented
|
||||
are purely for development
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Manimation(Scene):
|
||||
def construct(self):
|
||||
self.play(FadeIn(Circle()))
|
||||
|
||||
|
||||
Manager(Manimation).render()
|
||||
"""
|
||||
|
||||
def __init__(self, scene_name: str, camera: Camera, **kwargs) -> None:
|
||||
# renderer
|
||||
self.renderer = OpenGLRenderer(**kwargs)
|
||||
self.ctx = mp.get_context("spawn")
|
||||
def __init__(self, scene_cls: type[Scene]) -> None:
|
||||
# scene
|
||||
self.scene: Scene = scene_cls(self)
|
||||
|
||||
# setup
|
||||
self.processes: queue.Queue[mp.Process] = queue.Queue()
|
||||
self.manager = mp.Manager()
|
||||
self.manager_dict = self.manager.dict()
|
||||
if not isinstance(self.scene, Scene):
|
||||
raise ValueError(f"{self.scene!r} is not an instance of Scene")
|
||||
|
||||
# file writer
|
||||
self.camera = camera
|
||||
self.file_writer = FileWriter(scene_name) # TODO
|
||||
self.time = 0
|
||||
|
||||
# Initialize window, if applicable
|
||||
if config.preview:
|
||||
self.window = Window()
|
||||
else:
|
||||
self.window = None
|
||||
|
||||
# this must be done AFTER instantiating a window
|
||||
self.renderer = self.create_renderer()
|
||||
self.renderer.use_window()
|
||||
|
||||
def begin(self) -> None:
|
||||
# file writer
|
||||
self.file_writer = FileWriter(self.scene.get_default_scene_name()) # TODO
|
||||
|
||||
@property
|
||||
def camera(self) -> Camera:
|
||||
return self.scene.camera
|
||||
|
||||
def create_renderer(self) -> RendererProtocol:
|
||||
match config.renderer:
|
||||
case RendererType.OPENGL:
|
||||
return OpenGLRenderer()
|
||||
|
||||
case RendererType.CAIRO:
|
||||
return CairoRenderer()
|
||||
|
||||
case rendertype:
|
||||
raise ValueError(f"Invalid Config Renderer type {rendertype}")
|
||||
|
||||
def _setup(self) -> None:
|
||||
"""Set up processes and manager"""
|
||||
if self.file_writer.has_progress_display():
|
||||
self.scene.show_animation_progress = False
|
||||
|
||||
self.scene.setup()
|
||||
|
||||
self.virtual_animation_start_time = 0
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
|
||||
def render(self) -> None:
|
||||
"""
|
||||
Entry point to running a Manim class
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyScene(Scene):
|
||||
def construct(self):
|
||||
self.play(Create(Circle()))
|
||||
|
||||
|
||||
with tempconfig({"preview": True}):
|
||||
Manager(MyScene).render()
|
||||
"""
|
||||
self._render_first_pass()
|
||||
self._render_second_pass()
|
||||
self._interact()
|
||||
|
||||
def _render_first_pass(self) -> None:
|
||||
"""
|
||||
Temporarily use the normal single pass
|
||||
rendering system
|
||||
"""
|
||||
self._setup()
|
||||
|
||||
try:
|
||||
self.scene.construct()
|
||||
self._interact()
|
||||
except EndSceneEarlyException:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
# Get rid keyboard interrupt symbols
|
||||
print("", end="\r")
|
||||
self.file_writer.ended_with_interrupt = True
|
||||
self._tear_down()
|
||||
|
||||
def _render_second_pass(self) -> None:
|
||||
"""
|
||||
In the future, this method could be used
|
||||
for two pass rendering
|
||||
"""
|
||||
...
|
||||
|
||||
def get_time_progression(self, run_time: float) -> Iterable[float]:
|
||||
def _tear_down(self):
|
||||
self.scene.tear_down()
|
||||
|
||||
if config.save_last_frame:
|
||||
self._update_frame(0)
|
||||
|
||||
self.file_writer.finish()
|
||||
|
||||
if self.window is not None:
|
||||
self.window.close()
|
||||
self.window = None
|
||||
|
||||
def _interact(self) -> None:
|
||||
if self.window is None:
|
||||
return
|
||||
logger.info(
|
||||
"\nTips: Using the keys `d`, `f`, or `z` "
|
||||
+ "you can interact with the scene. "
|
||||
+ "Press `command + q` or `esc` to quit"
|
||||
)
|
||||
self.scene.skip_animations = False
|
||||
self.scene.refresh_static_mobjects()
|
||||
while not self.window.is_closing:
|
||||
# TODO: Replace with actual dt instead
|
||||
# of hardcoded dt
|
||||
dt = 1 / self.camera.fps
|
||||
self._update_frame(dt)
|
||||
|
||||
def _update_frame(self, dt: float):
|
||||
self.time += dt
|
||||
self.scene._update_mobjects(dt)
|
||||
|
||||
if self.window is not None:
|
||||
self.window.clear()
|
||||
|
||||
state = self.scene.get_state()
|
||||
self._render_frame(state)
|
||||
|
||||
if self.window is not None:
|
||||
self.window.swap_buffers()
|
||||
vt = self.time - self.virtual_animation_start_time
|
||||
rt = time.perf_counter() - self.real_animation_start_time
|
||||
if rt < vt:
|
||||
self._update_frame(0)
|
||||
|
||||
def _play(self, *animations: AnimationProtocol):
|
||||
self.scene.pre_play()
|
||||
|
||||
if self.window is not None:
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
||||
self.scene.begin_animations(animations)
|
||||
self._progress_through_animations(animations)
|
||||
self.scene.finish_animations(animations)
|
||||
|
||||
if self.scene.skip_animations and self.window is not None:
|
||||
self._update_frame(dt=0)
|
||||
|
||||
self.scene.post_play()
|
||||
|
||||
def _wait(
|
||||
self, duration: float, *, stop_condition: Callable[[], bool] | None = None
|
||||
):
|
||||
self.scene.pre_play()
|
||||
|
||||
update_mobjects = (
|
||||
self.scene.should_update_mobjects()
|
||||
) # TODO: this method needs to be implemented
|
||||
condition = stop_condition or (lambda: False)
|
||||
|
||||
last_t = 0
|
||||
for t in self._calc_time_progression(duration):
|
||||
if update_mobjects:
|
||||
dt, last_t = t - last_t, t
|
||||
self._update_frame(dt)
|
||||
if condition():
|
||||
break
|
||||
else:
|
||||
self.renderer.render_previous(self.camera)
|
||||
self.scene.post_play()
|
||||
|
||||
def _progress_through_animations(self, animations: Iterable[AnimationProtocol]):
|
||||
last_t = 0
|
||||
run_time = self._calc_runtime(animations)
|
||||
for t in self._calc_time_progression(run_time):
|
||||
dt, last_t = t - last_t, t
|
||||
self.scene._update_animations(animations, t, dt)
|
||||
self._update_frame(dt)
|
||||
|
||||
def _calc_time_progression(self, run_time: float) -> Iterable[float]:
|
||||
return np.arange(0, run_time, 1 / self.camera.fps)
|
||||
|
||||
def render_state(self, state: SceneState, parallel: bool = True) -> None:
|
||||
"""Launch a process (optionally in parallel)
|
||||
to render a frame
|
||||
"""
|
||||
if parallel and config.in_parallel:
|
||||
logger.warning("Not supported yet")
|
||||
self.render_frame(state)
|
||||
def _calc_runtime(self, animations: Iterable[AnimationProtocol]):
|
||||
return max(animation.get_run_time() for animation in animations)
|
||||
|
||||
# type state: SceneState
|
||||
def render_frame(self, state: SceneState) -> Any | None:
|
||||
"""Renders a frame based on a state"""
|
||||
data = self.send_scene_to_renderer(state)
|
||||
def _render_frame(self, state: SceneState) -> Any | None:
|
||||
"""Renders a frame based on a state, and writes it to a file"""
|
||||
data = self._send_scene_to_renderer(state)
|
||||
# result = self.file_writer.write(data)
|
||||
self.manager_dict[state.time] = data
|
||||
|
||||
def send_scene_to_renderer(self, state: SceneState):
|
||||
def _send_scene_to_renderer(self, state: SceneState):
|
||||
"""Renders the State"""
|
||||
result = self.renderer.render(self.camera, state.mobjects)
|
||||
result = self.renderer.render(self.scene.camera, state.mobjects)
|
||||
return result
|
||||
|
||||
def get_frames(self) -> list:
|
||||
"""Get a list of every frame produced by the
|
||||
manager.
|
||||
|
||||
.. warning::
|
||||
|
||||
This list is _not guarenteed_ to be sorted until
|
||||
after calling :meth:`.RenderManager.finish`
|
||||
"""
|
||||
return self.manager_dict
|
||||
|
||||
def finish(self) -> None:
|
||||
for process in self.processes.queue:
|
||||
process.join()
|
||||
self.manager_dict = dict(sorted(self.manager_dict.items()))
|
||||
|
|
|
|||
|
|
@ -1,14 +1,22 @@
|
|||
from abc import ABC, abstractclassmethod, abstractstaticmethod
|
||||
from typing import TYPE_CHECKING
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from manim import config
|
||||
from manim._config import logger
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.types.image_mobject import ImageMobject
|
||||
from manim.mobject.types.vectorized_mobject import VMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
|
||||
import moderngl as gl
|
||||
|
||||
from manim.camera.camera import Camera
|
||||
|
||||
ImageType: TypeAlias = np.ndarray
|
||||
|
||||
|
|
@ -20,15 +28,15 @@ class RendererData:
|
|||
class Renderer(ABC):
|
||||
def __init__(self):
|
||||
self.capabilities = [
|
||||
(OpenGLVMobject, self.render_vmobject),
|
||||
(ImageMobject, self.render_image),
|
||||
(OpenGLVMobject, self.render_vmobject), # type: ignore
|
||||
(ImageMobject, self.render_image), # type: ignore
|
||||
]
|
||||
|
||||
def render(self, camera, renderables: list[OpenGLVMobject]) -> None: # Image
|
||||
def render(self, camera, renderables: Iterable[OpenGLMobject]) -> None: # Image
|
||||
self.pre_render(camera)
|
||||
for mob in renderables:
|
||||
for type, render_func in self.capabilities:
|
||||
if isinstance(mob, type):
|
||||
for type_, render_func in self.capabilities:
|
||||
if isinstance(mob, type_):
|
||||
render_func(mob)
|
||||
break
|
||||
else:
|
||||
|
|
@ -37,27 +45,54 @@ class Renderer(ABC):
|
|||
)
|
||||
self.post_render()
|
||||
|
||||
def pre_render(self, camera):
|
||||
@abstractmethod
|
||||
def pre_render(self, camera: Camera):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def post_render(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def use_window(self):
|
||||
@abstractmethod
|
||||
def render_vmobject(self, mob: OpenGLVMobject):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractclassmethod
|
||||
def render_vmobject(self, mob: OpenGLVMobject) -> None:
|
||||
@abstractmethod
|
||||
def render_image(self, mob: ImageMobject):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RendererProtocol(Protocol):
|
||||
capabilities: Sequence[
|
||||
tuple[type[OpenGLMobject], Callable[[type[OpenGLMobject]], object]]
|
||||
]
|
||||
|
||||
def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None:
|
||||
...
|
||||
|
||||
def render_previous(self, camera: Camera) -> None:
|
||||
...
|
||||
|
||||
def pre_render(self, camera) -> object:
|
||||
...
|
||||
|
||||
def post_render(self) -> object:
|
||||
...
|
||||
|
||||
def use_window(self):
|
||||
...
|
||||
|
||||
def render_vmobject(self, mob: OpenGLVMobject) -> object:
|
||||
...
|
||||
|
||||
def render_mesh(self, mob) -> None:
|
||||
raise NotImplementedError
|
||||
...
|
||||
|
||||
def render_image(self, mob) -> None:
|
||||
raise NotImplementedError
|
||||
def render_image(self, mob: ImageMobject) -> None:
|
||||
...
|
||||
|
||||
def get_pixels(self) -> ImageType:
|
||||
raise NotImplementedError
|
||||
...
|
||||
|
||||
|
||||
# NOTE: The user should expect depth between renderers not to be handled discussed at 03.09.2023 Between jsonv and MrDiver
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import platform
|
|||
import random
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Sequence
|
||||
|
||||
import numpy as np
|
||||
from IPython.terminal import pt_inputhooks
|
||||
|
|
@ -24,8 +24,9 @@ from manim.mobject.frame import FullScreenRectangle
|
|||
from manim.mobject.mobject import Group, Point, _AnimationBuilder
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject as Mobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.renderer.render_manager import RenderManager
|
||||
from manim.utils.color import RED
|
||||
from manim.utils.deprecation import deprecated
|
||||
from manim.utils.exceptions import EndSceneEarlyException
|
||||
from manim.utils.family_ops import extract_mobject_family_members
|
||||
from manim.utils.iterables import list_difference_update
|
||||
from manim.utils.module_ops import get_module
|
||||
|
|
@ -33,12 +34,11 @@ from manim.utils.module_ops import get_module
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
from PIL.Image import Image
|
||||
|
||||
from manim.animation.protocol import AnimationProtocol as Animation
|
||||
from manim.animation.scene_buffer import SceneBuffer
|
||||
from manim.renderer.render_manager import Manager
|
||||
|
||||
# TODO: these keybindings should be made configureable
|
||||
# TODO: these keybindings should be made configurable
|
||||
|
||||
PAN_3D_KEY = "d"
|
||||
FRAME_SHIFT_KEY = "f"
|
||||
|
|
@ -57,6 +57,7 @@ class Scene:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
manager: Manager | None = None,
|
||||
window_config: dict = {},
|
||||
camera_config: dict = {},
|
||||
skip_animations: bool = False,
|
||||
|
|
@ -81,17 +82,10 @@ class Scene:
|
|||
self.embed_exception_mode = embed_exception_mode
|
||||
self.embed_error_sound = embed_error_sound
|
||||
|
||||
# Initialize window, if applicable
|
||||
if self.preview:
|
||||
from manim.renderer.opengl_renderer_window import Window
|
||||
|
||||
self.window = Window()
|
||||
else:
|
||||
self.window = None
|
||||
|
||||
# Core state of the scene
|
||||
self.camera: Camera = Camera()
|
||||
self.camera.save_state()
|
||||
self.manager = manager
|
||||
self.mobjects: list[Mobject] = []
|
||||
self.id_to_mobject_map: dict[int, Mobject] = {}
|
||||
self.num_plays: int = 0
|
||||
|
|
@ -100,12 +94,9 @@ class Scene:
|
|||
self.original_skipping_status: bool = self.skip_animations
|
||||
self.undo_stack = []
|
||||
self.redo_stack = []
|
||||
self.manager = RenderManager(self.get_default_scene_name(), camera=self.camera)
|
||||
|
||||
if self.start_at_animation_number is not None:
|
||||
self.skip_animations = True
|
||||
if self.manager.file_writer.has_progress_display():
|
||||
self.show_animation_progress = False
|
||||
|
||||
# Items associated with interaction
|
||||
self.mouse_point = Point()
|
||||
|
|
@ -138,68 +129,31 @@ class Scene:
|
|||
self.add(*buffer.to_add)
|
||||
buffer.clear()
|
||||
|
||||
def run(self) -> None:
|
||||
config.scene_name = str(self)
|
||||
self.virtual_animation_start_time: float = 0
|
||||
self.real_animation_start_time: float = time.time()
|
||||
@deprecated(message="Use Manager(Scene).render()")
|
||||
@classmethod
|
||||
def run(cls) -> None:
|
||||
from ..renderer.render_manager import Manager
|
||||
|
||||
self.setup()
|
||||
try:
|
||||
self.construct()
|
||||
# wait until all animations rendered in parallel
|
||||
self.manager.finish()
|
||||
self.interact()
|
||||
except EndScene:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
# Get rid keyboard interrupt symbols
|
||||
print("", end="\r")
|
||||
self.manager.file_writer.ended_with_interrupt = True
|
||||
self.tear_down()
|
||||
return Manager(cls).render()
|
||||
|
||||
render = run
|
||||
|
||||
def setup(self) -> None:
|
||||
"""
|
||||
This is meant to be implement by any scenes which
|
||||
are comonly subclassed, and have some common setup
|
||||
This method is used to set up scenes to do any setup
|
||||
involved before the construct method is called.
|
||||
"""
|
||||
pass
|
||||
|
||||
def construct(self) -> None:
|
||||
# Where all the animation happens
|
||||
# To be implemented in subclasses
|
||||
pass
|
||||
"""
|
||||
The entrypoint to animations in Manim.
|
||||
Should be overridden in the subclass to produce animations
|
||||
"""
|
||||
|
||||
def tear_down(self) -> None:
|
||||
self.stop_skipping()
|
||||
|
||||
if config.save_last_frame:
|
||||
self.update_frame(ignore_skipping=True)
|
||||
self.manager.file_writer.finish()
|
||||
|
||||
if self.window:
|
||||
self.window.close()
|
||||
self.window = None
|
||||
|
||||
def interact(self) -> None:
|
||||
"""
|
||||
If there is a window, enter a loop
|
||||
which updates the frame while under
|
||||
the hood calling the pyglet event loop
|
||||
This method is used to clean up scenes
|
||||
"""
|
||||
if self.window is None:
|
||||
return
|
||||
logger.info(
|
||||
"\nTips: Using the keys `d`, `f`, or `z` "
|
||||
+ "you can interact with the scene. "
|
||||
+ "Press `command + q` or `esc` to quit"
|
||||
)
|
||||
self.skip_animations = False
|
||||
self.refresh_static_mobjects()
|
||||
while not self.window.is_closing:
|
||||
self.update_frame(1 / self.camera.fps)
|
||||
|
||||
def embed(
|
||||
self,
|
||||
|
|
@ -288,7 +242,7 @@ class Scene:
|
|||
# Launch shell
|
||||
shell(
|
||||
local_ns=local_ns,
|
||||
# Pretend like we're embeding in the caller function, not here
|
||||
# Pretend like we're embedding in the caller function, not here
|
||||
stack_depth=2,
|
||||
# Specify that the present module is the caller's, not here
|
||||
module=get_module(caller_frame.f_globals["__file__"]),
|
||||
|
|
@ -296,54 +250,39 @@ class Scene:
|
|||
|
||||
# End scene when exiting an embed
|
||||
if close_scene_on_exit:
|
||||
raise EndScene()
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
# Only these methods should touch the camera
|
||||
def update_frame(self, dt: float = 0, ignore_skipping: bool = False) -> None:
|
||||
self.increment_time(dt)
|
||||
self.update_mobjects(dt)
|
||||
if self.skip_animations and not ignore_skipping:
|
||||
return
|
||||
|
||||
if self.window.is_closing:
|
||||
raise EndScene()
|
||||
|
||||
if self.window:
|
||||
self.window.clear()
|
||||
# self.camera.clear()
|
||||
state = self.get_state()
|
||||
self.manager.render_state(state)
|
||||
|
||||
if self.window:
|
||||
self.window.swap_buffers()
|
||||
vt = self.time - self.virtual_animation_start_time
|
||||
rt = time.time() - self.real_animation_start_time
|
||||
if rt < vt:
|
||||
self.update_frame(0)
|
||||
|
||||
# Related to updating
|
||||
|
||||
def update_mobjects(self, dt: float) -> None:
|
||||
def _update_mobjects(self, dt: float) -> None:
|
||||
for mobject in self.mobjects:
|
||||
mobject.update(dt)
|
||||
|
||||
def should_update_mobjects(self) -> bool:
|
||||
"""
|
||||
This is only called when a single Wait animation is played.
|
||||
This is called to check if a wait frame should be frozen
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool: does it have to be rerendered or is it static
|
||||
"""
|
||||
wait_animation = self.animations[0]
|
||||
if wait_animation.is_static_wait is None:
|
||||
should_update = (
|
||||
self.always_update_mobjects
|
||||
or self.updaters
|
||||
or wait_animation.stop_condition is not None
|
||||
or any(
|
||||
mob.has_time_based_updater()
|
||||
for mob in self.get_mobject_family_members()
|
||||
)
|
||||
)
|
||||
wait_animation.is_static_wait = not should_update
|
||||
return not wait_animation.is_static_wait
|
||||
# always rerender by returning True
|
||||
# TODO: Apply caching here
|
||||
return True
|
||||
# wait_animation = self.animations[0]
|
||||
# if wait_animation.is_static_wait is None:
|
||||
# should_update = (
|
||||
# self.always_update_mobjects
|
||||
# or self.updaters
|
||||
# or wait_animation.stop_condition is not None
|
||||
# or any(
|
||||
# mob.has_time_based_updater()
|
||||
# for mob in self.get_mobject_family_members()
|
||||
# )
|
||||
# )
|
||||
# wait_animation.is_static_wait = not should_update
|
||||
# return not wait_animation.is_static_wait
|
||||
|
||||
def has_time_based_updaters(self) -> bool:
|
||||
return any(
|
||||
|
|
@ -597,7 +536,7 @@ class Scene:
|
|||
if (self.end_at_animation_number is not None) and (
|
||||
self.num_plays >= self.end_at_animation_number
|
||||
):
|
||||
raise EndScene()
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
def stop_skipping(self) -> None:
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
|
@ -605,45 +544,6 @@ class Scene:
|
|||
|
||||
# Methods associated with running animations
|
||||
|
||||
def get_time_progression(
|
||||
self,
|
||||
run_time: float,
|
||||
n_iterations: int | None = None,
|
||||
desc: str = "",
|
||||
override_skip_animations: bool = False,
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
if self.skip_animations and not override_skip_animations:
|
||||
return [run_time]
|
||||
|
||||
times = np.arange(0, run_time, 1 / self.camera.fps)
|
||||
|
||||
# self.file_writer.set_progress_display_description(sub_desc=desc)
|
||||
|
||||
if self.show_animation_progress:
|
||||
return ProgressDisplay(
|
||||
times,
|
||||
total=n_iterations,
|
||||
leave=self.leave_progress_bars,
|
||||
ascii=True if platform.system() == "Windows" else None,
|
||||
desc=desc,
|
||||
)
|
||||
else:
|
||||
return times
|
||||
|
||||
def get_run_time(self, animations: Iterable[Animation]) -> float:
|
||||
return max([animation.get_run_time() for animation in animations])
|
||||
|
||||
def get_animation_time_progression(
|
||||
self, animations: Iterable[Animation]
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
animations = list(animations)
|
||||
run_time = self.get_run_time(animations)
|
||||
description = f"{self.num_plays} {animations[0]}"
|
||||
if len(animations) > 1:
|
||||
description += ", etc."
|
||||
time_progression = self.manager.get_time_progression(run_time)
|
||||
return time_progression
|
||||
|
||||
def get_wait_time_progression(
|
||||
self, duration: float, stop_condition: Callable[[], bool] | None = None
|
||||
) -> list[float] | np.ndarray | ProgressDisplay:
|
||||
|
|
@ -662,23 +562,13 @@ class Scene:
|
|||
# if not self.skip_animations:
|
||||
# self.file_writer.begin_animation()
|
||||
|
||||
if self.window:
|
||||
self.real_animation_start_time = time.time()
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
||||
self.refresh_static_mobjects()
|
||||
self.manager.begin()
|
||||
|
||||
def post_play(self):
|
||||
# if not self.skip_animations:
|
||||
# self.manager.file_writer.end_animation()
|
||||
|
||||
if self.skip_animations and self.window is not None:
|
||||
# Show some quick frames along the way
|
||||
self.update_frame(dt=0, ignore_skipping=True)
|
||||
|
||||
self.num_plays += 1
|
||||
self.manager.finish()
|
||||
|
||||
def refresh_static_mobjects(self) -> None:
|
||||
# self.camera.refresh_static_mobjects()
|
||||
|
|
@ -689,20 +579,14 @@ class Scene:
|
|||
animation.begin()
|
||||
self.process_buffer(animation.buffer)
|
||||
|
||||
def progress_through_animations(self, animations: Iterable[Animation]) -> None:
|
||||
last_t = 0
|
||||
for t in self.get_animation_time_progression(animations):
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
for animation in animations:
|
||||
animation.update_mobjects(dt)
|
||||
alpha = t / animation.get_run_time()
|
||||
animation.interpolate(alpha)
|
||||
if animation.apply_buffer:
|
||||
self.process_buffer(animation.buffer)
|
||||
animation.apply_buffer = False
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
def _update_animations(self, animations: Iterable[Animation], t: float, dt: float):
|
||||
for animation in animations:
|
||||
animation.update_mobjects(dt)
|
||||
alpha = t / animation.get_run_time()
|
||||
animation.interpolate(alpha)
|
||||
if animation.apply_buffer:
|
||||
self.process_buffer(animation.buffer)
|
||||
animation.apply_buffer = False
|
||||
|
||||
def finish_animations(self, animations: Iterable[Animation]) -> None:
|
||||
for animation in animations:
|
||||
|
|
@ -710,9 +594,9 @@ class Scene:
|
|||
self.process_buffer(animation.buffer)
|
||||
|
||||
if self.skip_animations:
|
||||
self.update_mobjects(self.get_run_time(animations))
|
||||
self._update_mobjects(self.manager._calc_runtime(animations))
|
||||
else:
|
||||
self.update_mobjects(0)
|
||||
self._update_mobjects(0)
|
||||
|
||||
def play(
|
||||
self,
|
||||
|
|
@ -727,46 +611,40 @@ class Scene:
|
|||
animations = [prepare_animation(x) for x in proto_animations]
|
||||
for anim in animations:
|
||||
anim.update_rate_info(run_time, rate_func, lag_ratio)
|
||||
self.pre_play()
|
||||
self.begin_animations(animations)
|
||||
self.progress_through_animations(animations)
|
||||
self.finish_animations(animations)
|
||||
self.post_play()
|
||||
|
||||
# NOTE: Should be changed at some point with the 2 pass rendering system 21.06.2024
|
||||
self.manager._play(*animations)
|
||||
|
||||
def wait(
|
||||
self,
|
||||
duration: float = DEFAULT_WAIT_TIME,
|
||||
stop_condition: Callable[[], bool] = None,
|
||||
note: str = None,
|
||||
stop_condition: Callable[[], bool] | None = None,
|
||||
note: str | None = None,
|
||||
ignore_presenter_mode: bool = False,
|
||||
):
|
||||
self.pre_play()
|
||||
self.update_mobjects(dt=0) # Any problems with this?
|
||||
if (
|
||||
self.presenter_mode
|
||||
and not self.skip_animations
|
||||
and not ignore_presenter_mode
|
||||
):
|
||||
if note:
|
||||
logger.info(note)
|
||||
self.hold_loop()
|
||||
else:
|
||||
time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
last_t = 0
|
||||
for t in time_progression:
|
||||
dt = t - last_t
|
||||
last_t = t
|
||||
self.update_frame(dt)
|
||||
self.emit_frame()
|
||||
if stop_condition is not None and stop_condition():
|
||||
break
|
||||
self.refresh_static_mobjects()
|
||||
self.post_play()
|
||||
|
||||
def hold_loop(self):
|
||||
while self.hold_on_wait:
|
||||
self.update_frame(dt=1 / self.camera.fps)
|
||||
self.hold_on_wait = True
|
||||
self.manager._wait(duration, stop_condition=stop_condition)
|
||||
# self.pre_play()
|
||||
# self.update_mobjects(dt=0) # Any problems with this?
|
||||
# if (
|
||||
# self.presenter_mode
|
||||
# and not self.skip_animations
|
||||
# and not ignore_presenter_mode
|
||||
# ):
|
||||
# if note:
|
||||
# logger.info(note)
|
||||
# self.hold_loop()
|
||||
# else:
|
||||
# time_progression = self.get_wait_time_progression(duration, stop_condition)
|
||||
# last_t = 0
|
||||
# for t in time_progression:
|
||||
# dt = t - last_t
|
||||
# last_t = t
|
||||
# self.update_frame(dt)
|
||||
# self.emit_frame()
|
||||
# if stop_condition is not None and stop_condition():
|
||||
# break
|
||||
# self.refresh_static_mobjects()
|
||||
# self.post_play()
|
||||
|
||||
def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60):
|
||||
self.wait(max_time, stop_condition=stop_condition)
|
||||
|
|
@ -995,8 +873,8 @@ class SceneState:
|
|||
self.mobjects_to_copies[mob] = mob.copy()
|
||||
|
||||
@property
|
||||
def mobjects(self) -> list[Mobject]:
|
||||
return self.mobjects_to_copies
|
||||
def mobjects(self) -> Sequence[Mobject]:
|
||||
return tuple(self.mobjects_to_copies.keys())
|
||||
|
||||
def __eq__(self, state: Any) -> bool:
|
||||
return isinstance(state, SceneState) and all(
|
||||
|
|
@ -1007,6 +885,9 @@ class SceneState:
|
|||
)
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__} of {len(self.mobjects_to_copies)} Mobjects"
|
||||
|
||||
def mobjects_match(self, state: SceneState):
|
||||
return self.mobjects_to_copies == state.mobjects_to_copies
|
||||
|
||||
|
|
@ -1023,7 +904,3 @@ class SceneState:
|
|||
scene.mobjects = [
|
||||
mob.become(mob_copy) for mob, mob_copy in self.mobjects_to_copies.items()
|
||||
]
|
||||
|
||||
|
||||
class EndScene(Exception):
|
||||
pass
|
||||
|
|
|
|||
286
poetry.lock
generated
286
poetry.lock
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue