Skip to content

Commit 4693af3

Browse files
authored
Renderer refactor (#532)
Factors Scene.camera, Scene.file_writer, and rendering logic within Scene into a new CairoRenderer class
2 parents f245e08 + 44a0dee commit 4693af3

25 files changed

+531
-562
lines changed

docs/source/examples/3d.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
]), v_min=0, v_max=TAU, u_min=-PI / 2, u_max=PI / 2,
3333
checkerboard_colors=[RED_D, RED_E], resolution=(15, 32)
3434
)
35-
self.camera.light_source.move_to(3*IN) # changes the source of the light
35+
self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light
3636
self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
3737
self.add(axes, sphere)
3838

manim/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from .animation.transform import *
2222
from .animation.update import *
2323

24+
from .renderer.cairo_renderer import *
25+
2426
from .camera.camera import *
2527
from .camera.mapping_camera import *
2628
from .camera.moving_camera import *

manim/__main__.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
1-
import inspect
21
import os
32
import platform
4-
import subprocess as sp
53
import sys
6-
import re
74
import traceback
8-
import importlib.util
9-
import types
105

11-
from . import constants, logger, console, file_writer_config
6+
from . import logger, file_writer_config
127
from .config.config import camera_config, args
138
from .config import cfg_subcmds
149
from .utils.module_ops import (
1510
get_module,
1611
get_scene_classes_from_module,
1712
get_scenes_to_render,
1813
)
19-
from .scene.scene import Scene
2014
from .utils.file_ops import open_file as open_media_file
2115
from .grpc.impl import frame_server_impl
2216

@@ -79,7 +73,7 @@ def main():
7973
else:
8074
scene = SceneClass()
8175
scene.render()
82-
open_file_if_needed(scene.file_writer)
76+
open_file_if_needed(scene.renderer.file_writer)
8377
except Exception:
8478
print("\n\n")
8579
traceback.print_exc()

manim/camera/camera.py

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ..utils.simple_functions import fdiv
2929
from ..utils.space_ops import angle_of_vector
3030
from ..utils.space_ops import get_norm
31+
from ..utils.family import extract_mobject_family_members
3132

3233

3334
class Camera(object):
@@ -66,7 +67,7 @@ class Camera(object):
6667
"use_z_index": True,
6768
}
6869

69-
def __init__(self, background=None, **kwargs):
70+
def __init__(self, video_quality_config, background=None, **kwargs):
7071
"""Initialises the Camera.
7172
7273
Parameters
@@ -363,33 +364,6 @@ def set_frame_to_background(self, background):
363364

364365
####
365366

366-
# TODO, it's weird that this is part of camera.
367-
# Clearly it should live elsewhere.
368-
def extract_mobject_family_members(self, mobjects, only_those_with_points=False):
369-
"""Returns a list of the types of mobjects and
370-
their family members present.
371-
372-
Parameters
373-
----------
374-
mobjects : Mobject
375-
The Mobjects currently in the Scene
376-
only_those_with_points : bool, optional
377-
Whether or not to only do this for
378-
those mobjects that have points. By default False
379-
380-
Returns
381-
-------
382-
list
383-
list of the mobjects and family members.
384-
"""
385-
if only_those_with_points:
386-
method = Mobject.family_members_with_points
387-
else:
388-
method = Mobject.get_family
389-
if self.use_z_index:
390-
mobjects = sorted(mobjects, key=lambda m: m.z_index)
391-
return remove_list_redundancies(list(it.chain(*[method(m) for m in mobjects])))
392-
393367
def get_mobjects_to_display(
394368
self, mobjects, include_submobjects=True, excluded_mobjects=None
395369
):
@@ -411,11 +385,13 @@ def get_mobjects_to_display(
411385
list of mobjects
412386
"""
413387
if include_submobjects:
414-
mobjects = self.extract_mobject_family_members(
415-
mobjects, only_those_with_points=True
388+
mobjects = extract_mobject_family_members(
389+
mobjects, use_z_index=self.use_z_index, only_those_with_points=True
416390
)
417391
if excluded_mobjects:
418-
all_excluded = self.extract_mobject_family_members(excluded_mobjects)
392+
all_excluded = extract_mobject_family_members(
393+
excluded_mobjects, use_z_index=self.use_z_index
394+
)
419395
mobjects = list_difference_update(mobjects, all_excluded)
420396
return mobjects
421397

manim/camera/moving_camera.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class MovingCamera(Camera):
4646
"default_frame_stroke_width": 0,
4747
}
4848

49-
def __init__(self, frame=None, **kwargs):
49+
def __init__(self, video_quality_config, frame=None, **kwargs):
5050
"""
5151
frame is a Mobject, (should almost certainly be a rectangle)
5252
determining which region of space the camera displys
@@ -59,7 +59,7 @@ def __init__(self, frame=None, **kwargs):
5959
self.default_frame_stroke_width,
6060
)
6161
self.frame = frame
62-
Camera.__init__(self, **kwargs)
62+
Camera.__init__(self, video_quality_config, **kwargs)
6363

6464
# TODO, make these work for a rotated frame
6565
@property

manim/camera/multi_camera.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ class MultiCamera(MovingCamera):
1414
"allow_cameras_to_capture_their_own_display": False,
1515
}
1616

17-
def __init__(self, *image_mobjects_from_cameras, **kwargs):
17+
def __init__(
18+
self, video_quality_config, image_mobjects_from_cameras=None, **kwargs
19+
):
1820
"""Initalises the MultiCamera
1921
2022
Parameters:
@@ -25,9 +27,10 @@ def __init__(self, *image_mobjects_from_cameras, **kwargs):
2527
Any valid keyword arguments of MovingCamera.
2628
"""
2729
self.image_mobjects_from_cameras = []
28-
for imfc in image_mobjects_from_cameras:
29-
self.add_image_mobject_from_camera(imfc)
30-
MovingCamera.__init__(self, **kwargs)
30+
if image_mobjects_from_cameras is not None:
31+
for imfc in image_mobjects_from_cameras:
32+
self.add_image_mobject_from_camera(imfc)
33+
MovingCamera.__init__(self, video_quality_config, **kwargs)
3134

3235
def add_image_mobject_from_camera(self, image_mobject_from_camera):
3336
"""Adds an ImageMobject that's been obtained from the camera

manim/grpc/impl/frame_server_impl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def GetFrameAtTime(self, request, context):
9898
selected_scene.static_image,
9999
)
100100
serialized_mobject_list, duration = selected_scene.add_frame(
101-
selected_scene.get_frame()
101+
selected_scene.renderer.get_frame()
102102
)
103103
resp = list_to_frame_response(
104104
selected_scene, duration, serialized_mobject_list

manim/renderer/__init__.py

Whitespace-only changes.

manim/renderer/cairo_renderer.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import numpy as np
2+
from .. import config, camera_config, file_writer_config
3+
from ..utils.iterables import list_update
4+
from ..utils.exceptions import EndSceneEarlyException
5+
from ..constants import DEFAULT_WAIT_TIME
6+
from ..scene.scene_file_writer import SceneFileWriter
7+
from ..utils.caching import handle_caching_play, handle_caching_wait
8+
from ..camera.camera import Camera
9+
10+
11+
def pass_scene_reference(func):
12+
def wrapper(self, scene, *args, **kwargs):
13+
func(self, scene, *args, **kwargs)
14+
15+
return wrapper
16+
17+
18+
def handle_play_like_call(func):
19+
"""
20+
This method is used internally to wrap the
21+
passed function, into a function that
22+
actually writes to the video stream.
23+
Simultaneously, it also adds to the number
24+
of animations played.
25+
26+
Parameters
27+
----------
28+
func : function
29+
The play() like function that has to be
30+
written to the video file stream.
31+
32+
Returns
33+
-------
34+
function
35+
The play() like function that can now write
36+
to the video file stream.
37+
"""
38+
39+
def wrapper(self, scene, *args, **kwargs):
40+
allow_write = not file_writer_config["skip_animations"]
41+
self.file_writer.begin_animation(allow_write)
42+
func(self, scene, *args, **kwargs)
43+
self.file_writer.end_animation(allow_write)
44+
self.num_plays += 1
45+
46+
return wrapper
47+
48+
49+
class CairoRenderer:
50+
"""A renderer using Cairo.
51+
52+
num_plays : Number of play() functions in the scene.
53+
time: time elapsed since initialisation of scene.
54+
"""
55+
56+
def __init__(self, camera_class=None, **kwargs):
57+
# All of the following are set to EITHER the value passed via kwargs,
58+
# OR the value stored in the global config dict at the time of
59+
# _instance construction_. Before, they were in the CONFIG dict, which
60+
# is a class attribute and is defined at the time of _class
61+
# definition_. This did not allow for creating two Cameras with
62+
# different configurations in the same session.
63+
self.file_writer = None
64+
self.video_quality_config = {}
65+
for attr in [
66+
"pixel_height",
67+
"pixel_width",
68+
"frame_height",
69+
"frame_width",
70+
"frame_rate",
71+
]:
72+
self.video_quality_config[attr] = kwargs.get(attr, config[attr])
73+
camera_cls = camera_class if camera_class is not None else Camera
74+
self.camera = camera_cls(self.video_quality_config, **camera_config)
75+
self.original_skipping_status = file_writer_config["skip_animations"]
76+
self.animations_hashes = []
77+
self.num_plays = 0
78+
self.time = 0
79+
80+
def init(self, scene):
81+
self.file_writer = SceneFileWriter(
82+
self,
83+
self.video_quality_config,
84+
scene.__class__.__name__,
85+
**file_writer_config,
86+
)
87+
88+
@pass_scene_reference
89+
@handle_caching_play
90+
@handle_play_like_call
91+
def play(self, scene, *args, **kwargs):
92+
scene.play_internal(*args, **kwargs)
93+
94+
@pass_scene_reference
95+
@handle_caching_wait
96+
@handle_play_like_call
97+
def wait(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None):
98+
scene.wait_internal(duration=duration, stop_condition=stop_condition)
99+
100+
def update_frame( # TODO Description in Docstring
101+
self,
102+
scene,
103+
mobjects=None,
104+
background=None,
105+
include_submobjects=True,
106+
ignore_skipping=True,
107+
**kwargs,
108+
):
109+
"""Update the frame.
110+
111+
Parameters
112+
----------
113+
mobjects: list, optional
114+
list of mobjects
115+
116+
background: np.ndarray, optional
117+
Pixel Array for Background.
118+
119+
include_submobjects: bool, optional
120+
121+
ignore_skipping : bool, optional
122+
123+
**kwargs
124+
125+
"""
126+
if file_writer_config["skip_animations"] and not ignore_skipping:
127+
return
128+
if mobjects is None:
129+
mobjects = list_update(
130+
scene.mobjects,
131+
scene.foreground_mobjects,
132+
)
133+
if background is not None:
134+
self.camera.set_frame_to_background(background)
135+
else:
136+
self.camera.reset()
137+
138+
kwargs["include_submobjects"] = include_submobjects
139+
self.camera.capture_mobjects(mobjects, **kwargs)
140+
141+
def get_frame(self):
142+
"""
143+
Gets the current frame as NumPy array.
144+
145+
Returns
146+
-------
147+
np.array
148+
NumPy array of pixel values of each pixel in screen.
149+
The shape of the array is height x width x 3
150+
"""
151+
return np.array(self.camera.pixel_array)
152+
153+
def add_frame(self, frame, num_frames=1):
154+
"""
155+
Adds a frame to the video_file_stream
156+
157+
Parameters
158+
----------
159+
frame : numpy.ndarray
160+
The frame to add, as a pixel array.
161+
num_frames: int
162+
The number of times to add frame.
163+
"""
164+
dt = 1 / self.camera.frame_rate
165+
self.time += num_frames * dt
166+
if file_writer_config["skip_animations"]:
167+
return
168+
for _ in range(num_frames):
169+
self.file_writer.write_frame(frame)
170+
171+
def show_frame(self):
172+
"""
173+
Opens the current frame in the Default Image Viewer
174+
of your system.
175+
"""
176+
self.update_frame(ignore_skipping=True)
177+
self.camera.get_image().show()
178+
179+
def update_skipping_status(self):
180+
"""
181+
This method is used internally to check if the current
182+
animation needs to be skipped or not. It also checks if
183+
the number of animations that were played correspond to
184+
the number of animations that need to be played, and
185+
raises an EndSceneEarlyException if they don't correspond.
186+
"""
187+
if file_writer_config["from_animation_number"]:
188+
if self.num_plays < file_writer_config["from_animation_number"]:
189+
file_writer_config["skip_animations"] = True
190+
if file_writer_config["upto_animation_number"]:
191+
if self.num_plays > file_writer_config["upto_animation_number"]:
192+
file_writer_config["skip_animations"] = True
193+
raise EndSceneEarlyException()
194+
195+
def revert_to_original_skipping_status(self):
196+
"""
197+
Forces the scene to go back to its original skipping status,
198+
by setting skip_animations to whatever it reads
199+
from original_skipping_status.
200+
201+
Returns
202+
-------
203+
Scene
204+
The Scene, with the original skipping status.
205+
"""
206+
if hasattr(self, "original_skipping_status"):
207+
file_writer_config["skip_animations"] = self.original_skipping_status
208+
return self
209+
210+
def finish(self, scene):
211+
file_writer_config["skip_animations"] = False
212+
self.file_writer.finish()
213+
if file_writer_config["save_last_frame"]:
214+
self.update_frame(scene, ignore_skipping=True)
215+
self.file_writer.save_final_image(self.camera.get_image())

manim/scene/js_scene.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ def progress_through_animations(self):
109109
logger.error(e)
110110
self.animation_finished.wait()
111111

112-
@scene.handle_play_like_call
113112
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
114113
self.update_mobjects(dt=0) # Any problems with this?
115114
self.animations = []

0 commit comments

Comments
 (0)