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 sys
import copy
from manimlib.scene.scene import Scene
from manimlib.config import get_custom_config
@ -38,7 +39,7 @@ def prompt_user_for_choice(scene_classes):
"\nScene Name or Number: "
)
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(",")
]
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):
if config["write_all"]:
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
for scene_class in scene_classes:
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)
result.append(scene)
found = True

View file

@ -282,48 +282,42 @@ class Scene(object):
self.skip_time += self.time
# 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:
times = [run_time]
return [run_time]
else:
step = 1 / self.camera.frame_rate
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,
total=n_iterations,
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):
return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations):
run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([
f"Animation {self.num_plays}: {animations[0]}",
", etc." if len(animations) > 1 else "",
]))
description = f"{self.num_plays} {animations[0]}"
if len(animations) > 1:
description += ", etc."
time_progression = self.get_time_progression(run_time, desc=description)
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:
time_progression = self.get_time_progression(
duration,
n_iterations=-1, # So it doesn't show % progress
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
kw["n_iterations"] = -1 # So it doesn't show % progress
kw["override_skip_animations"] = True
return self.get_time_progression(duration, **kw)
def anims_from_play_args(self, *args, **kwargs):
"""
@ -488,13 +482,9 @@ class Scene(object):
time_progression.close()
break
self.unlock_mobject_data()
elif self.skip_animations:
# Do nothing
return self
else:
self.update_frame(duration)
n_frames = int(duration * self.camera.frame_rate)
for n in range(n_frames):
for n in self.get_wait_time_progression(duration):
self.emit_frame()
return self

View file

@ -5,6 +5,7 @@ import subprocess as sp
import os
import sys
import platform
from tqdm import tqdm as ProgressDisplay
from manimlib.constants import FFMPEG_BIN
from manimlib.utils.config_ops import digest_config
@ -35,12 +36,15 @@ class SceneFileWriter(object):
"open_file_upon_completion": False,
"show_file_location_upon_completion": False,
"quiet": False,
"total_frames": 0,
"progress_description_len": 35,
}
def __init__(self, scene, **kwargs):
digest_config(self, kwargs)
self.scene = scene
self.writing_process = None
self.has_progress_display = False
self.init_output_directories()
self.init_audio()
@ -205,15 +209,37 @@ class SceneFileWriter(object):
command += [self.temp_file_path]
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):
if self.write_to_movie:
raw_bytes = camera.get_raw_fbo_data()
self.writing_process.stdin.write(raw_bytes)
if self.has_progress_display:
self.progress_display.update()
def close_movie_pipe(self):
self.writing_process.stdin.close()
self.writing_process.wait()
self.writing_process.terminate()
if self.has_progress_display:
self.progress_display.close()
shutil.move(self.temp_file_path, self.final_file_path)
def combine_movie_files(self):