Create single progress display for full scene render

When a scene is written to file, it will now do a preliminary run of a copy of the scene with skip_animations turned on to count the total frames, which has the added benefit of catching runtime errors early, and allowing an quicker preview of the last frame to be sure everything will render as expected.

The Progress display bars for individual animations are replaced with a more global progress display bar showing the full render time for the scene.

This has the downside that all the non-rendering computations in a scene are run twice, so any scene with slow computations unrelated to rendering will take longer. But those are rarer, so the benefits seem worth it.
This commit is contained in:
Grant Sanderson 2021-11-30 11:41:33 -08:00
parent 49743daf32
commit 9dd1f47dab
3 changed files with 69 additions and 31 deletions

View file

@ -1,5 +1,6 @@
import inspect import inspect
import sys import sys
import copy
from manimlib.scene.scene import Scene from manimlib.scene.scene import Scene
from manimlib.config import get_custom_config from manimlib.config import get_custom_config
@ -38,7 +39,7 @@ def prompt_user_for_choice(scene_classes):
"\nScene Name or Number: " "\nScene Name or Number: "
) )
return [ return [
name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str)-1] name_to_class[split_str] if not split_str.isnumeric() else scene_classes[int(split_str) - 1]
for split_str in user_input.replace(" ", "").split(",") for split_str in user_input.replace(" ", "").split(",")
] ]
except IndexError: except IndexError:
@ -67,6 +68,24 @@ def get_scene_config(config):
]) ])
def compute_total_frames(scene_class, scene_config):
"""
When a scene is being written to file, a copy of the scene is run with
skip_animations set to true so as to count how many frames it will require.
This allows for a total progress bar on rendering, and also allows runtime
errors to be exposed preemptively for long running scenes. The final frame
is saved by default, so that one can more quickly check that the last frame
looks as expected.
"""
pre_config = copy.deepcopy(scene_config)
pre_config["file_writer_config"]["write_to_movie"] = False
pre_config["file_writer_config"]["save_last_frame"] = True
pre_config["skip_animations"] = True
pre_scene = scene_class(**pre_config)
pre_scene.run()
return int(pre_scene.time * scene_config["camera_config"]["frame_rate"])
def get_scenes_to_render(scene_classes, scene_config, config): def get_scenes_to_render(scene_classes, scene_config, config):
if config["write_all"]: if config["write_all"]:
return [sc(**scene_config) for sc in scene_classes] return [sc(**scene_config) for sc in scene_classes]
@ -76,6 +95,9 @@ def get_scenes_to_render(scene_classes, scene_config, config):
found = False found = False
for scene_class in scene_classes: for scene_class in scene_classes:
if scene_class.__name__ == scene_name: if scene_class.__name__ == scene_name:
fw_config = scene_config["file_writer_config"]
if fw_config["write_to_movie"]:
fw_config["total_frames"] = compute_total_frames(scene_class, scene_config)
scene = scene_class(**scene_config) scene = scene_class(**scene_config)
result.append(scene) result.append(scene)
found = True found = True

View file

@ -282,48 +282,42 @@ class Scene(object):
self.skip_time += self.time self.skip_time += self.time
# Methods associated with running animations # Methods associated with running animations
def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False): def get_time_progression(self, run_time, n_iterations=None, desc="", override_skip_animations=False):
if self.skip_animations and not override_skip_animations: if self.skip_animations and not override_skip_animations:
times = [run_time] return [run_time]
else: else:
step = 1 / self.camera.frame_rate step = 1 / self.camera.frame_rate
times = np.arange(0, run_time, step) times = np.arange(0, run_time, step)
time_progression = ProgressDisplay(
if self.file_writer.has_progress_display:
self.file_writer.set_progress_display_subdescription(desc)
return times
return ProgressDisplay(
times, times,
total=n_iterations, total=n_iterations,
leave=self.leave_progress_bars, leave=self.leave_progress_bars,
ascii=True if platform.system() == 'Windows' else None ascii=True if platform.system() == 'Windows' else None,
desc=desc,
) )
return time_progression
def get_run_time(self, animations): def get_run_time(self, animations):
return np.max([animation.run_time for animation in animations]) return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations): def get_animation_time_progression(self, animations):
run_time = self.get_run_time(animations) run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time) description = f"{self.num_plays} {animations[0]}"
time_progression.set_description("".join([ if len(animations) > 1:
f"Animation {self.num_plays}: {animations[0]}", description += ", etc."
", etc." if len(animations) > 1 else "", time_progression = self.get_time_progression(run_time, desc=description)
]))
return time_progression return time_progression
def get_wait_time_progression(self, duration, stop_condition): def get_wait_time_progression(self, duration, stop_condition=None):
kw = {"desc": f"{self.num_plays} Waiting"}
if stop_condition is not None: if stop_condition is not None:
time_progression = self.get_time_progression( kw["n_iterations"] = -1 # So it doesn't show % progress
duration, kw["override_skip_animations"] = True
n_iterations=-1, # So it doesn't show % progress return self.get_time_progression(duration, **kw)
override_skip_animations=True
)
time_progression.set_description(
"Waiting for {}".format(stop_condition.__name__)
)
else:
time_progression = self.get_time_progression(duration)
time_progression.set_description(
"Waiting {}".format(self.num_plays)
)
return time_progression
def anims_from_play_args(self, *args, **kwargs): def anims_from_play_args(self, *args, **kwargs):
""" """
@ -488,13 +482,9 @@ class Scene(object):
time_progression.close() time_progression.close()
break break
self.unlock_mobject_data() self.unlock_mobject_data()
elif self.skip_animations:
# Do nothing
return self
else: else:
self.update_frame(duration) self.update_frame(duration)
n_frames = int(duration * self.camera.frame_rate) for n in self.get_wait_time_progression(duration):
for n in range(n_frames):
self.emit_frame() self.emit_frame()
return self return self

View file

@ -5,6 +5,7 @@ import subprocess as sp
import os import os
import sys import sys
import platform import platform
from tqdm import tqdm as ProgressDisplay
from manimlib.constants import FFMPEG_BIN from manimlib.constants import FFMPEG_BIN
from manimlib.utils.config_ops import digest_config from manimlib.utils.config_ops import digest_config
@ -35,12 +36,15 @@ class SceneFileWriter(object):
"open_file_upon_completion": False, "open_file_upon_completion": False,
"show_file_location_upon_completion": False, "show_file_location_upon_completion": False,
"quiet": False, "quiet": False,
"total_frames": 0,
"progress_description_len": 35,
} }
def __init__(self, scene, **kwargs): def __init__(self, scene, **kwargs):
digest_config(self, kwargs) digest_config(self, kwargs)
self.scene = scene self.scene = scene
self.writing_process = None self.writing_process = None
self.has_progress_display = False
self.init_output_directories() self.init_output_directories()
self.init_audio() self.init_audio()
@ -205,15 +209,37 @@ class SceneFileWriter(object):
command += [self.temp_file_path] command += [self.temp_file_path]
self.writing_process = sp.Popen(command, stdin=sp.PIPE) self.writing_process = sp.Popen(command, stdin=sp.PIPE)
if self.total_frames > 0:
self.progress_display = ProgressDisplay(
range(self.total_frames),
leave=False,
ascii=True if platform.system() == 'Windows' else None,
desc="Full render: "
)
self.has_progress_display = True
def set_progress_display_subdescription(self, desc):
desc_len = self.progress_description_len
full_desc = f"Full render ({desc})"
if len(full_desc) > desc_len:
full_desc = full_desc[:desc_len - 4] + "...)"
else:
full_desc += " " * (desc_len - len(full_desc))
self.progress_display.set_description(full_desc)
def write_frame(self, camera): def write_frame(self, camera):
if self.write_to_movie: if self.write_to_movie:
raw_bytes = camera.get_raw_fbo_data() raw_bytes = camera.get_raw_fbo_data()
self.writing_process.stdin.write(raw_bytes) self.writing_process.stdin.write(raw_bytes)
if self.has_progress_display:
self.progress_display.update()
def close_movie_pipe(self): def close_movie_pipe(self):
self.writing_process.stdin.close() self.writing_process.stdin.close()
self.writing_process.wait() self.writing_process.wait()
self.writing_process.terminate() self.writing_process.terminate()
if self.has_progress_display:
self.progress_display.close()
shutil.move(self.temp_file_path, self.final_file_path) shutil.move(self.temp_file_path, self.final_file_path)
def combine_movie_files(self): def combine_movie_files(self):