mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
[EXPERIMENTAL] Make play_logic and file_writer tests pass + Manager fixes (#4559)
* Fix embed dt calculation, Arrow stroke_width bug, and shader compatibility * Restored lost fixes from b78ccba: Arrow stroke_width, Shaders AA, Manager interaction * fix(core): resolve recursion in set_fill, ManimColor opacity error, and restore add_sound * Fix(Experimental): Restore functionality and stabilize architecture - Fix Arrow stroke_width initialization (float vs list bug) and restore scaling logic. - Implement 'save_last_frame' logic by integrating it with skipping mechanism. - Refactor Manager.post_construct to ensure correct exit and image saving behavior. - Fix transparency issues in OpenGLRenderer (background_opacity and shader alpha). - Update shader code to use modern 'texture()' syntax instead of 'texture2D()'. - Fix VMobject recursion error in set_fill and ManimColor initialization. - Restore audio connectivity in Scene.add_sound. - Update tests to align with new Manager(Scene) architecture and document pending TODOs. - Detailed rationale in CHANGES_SUMMARY.md. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
84870ed0a9
commit
8b5d48d424
12 changed files with 107 additions and 59 deletions
|
|
@ -676,7 +676,7 @@ class FileWriter(FileWriterProtocol):
|
|||
output_container.mux(packet)
|
||||
|
||||
else:
|
||||
output_stream = output_container.add_stream_from_template(
|
||||
output_stream = output_container.add_stream(
|
||||
template=partial_movies_stream,
|
||||
)
|
||||
if config.transparent and config.movie_file_extension == ".webm":
|
||||
|
|
@ -768,12 +768,8 @@ class FileWriter(FileWriterProtocol):
|
|||
output_container = av.open(
|
||||
str(temp_file_path), mode="w", options=av_options
|
||||
)
|
||||
output_video_stream = output_container.add_stream_from_template(
|
||||
template=video_stream
|
||||
)
|
||||
output_audio_stream = output_container.add_stream_from_template(
|
||||
template=audio_stream
|
||||
)
|
||||
output_video_stream = output_container.add_stream(template=video_stream)
|
||||
output_audio_stream = output_container.add_stream(template=audio_stream)
|
||||
|
||||
for packet in video_input.demux(video_stream):
|
||||
# We need to skip the "flushing" packets that `demux` generates.
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class Manager(Generic[SceneT]):
|
|||
self._write_files = config.write_to_movie
|
||||
|
||||
# internal state
|
||||
self._skipping = False
|
||||
self._skipping = config.save_last_frame
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.scene!r}) at time {self.time:.2f}s"
|
||||
|
|
@ -113,7 +113,10 @@ class Manager(Generic[SceneT]):
|
|||
-------
|
||||
An instance of a renderer
|
||||
"""
|
||||
renderer = OpenGLRenderer()
|
||||
renderer = OpenGLRenderer(
|
||||
background_color=config.background_color,
|
||||
background_opacity=config.background_opacity,
|
||||
)
|
||||
if config.preview:
|
||||
renderer.use_window()
|
||||
return renderer
|
||||
|
|
@ -211,10 +214,13 @@ class Manager(Generic[SceneT]):
|
|||
|
||||
def post_construct(self) -> None:
|
||||
"""Run post-construct hooks, and clean up the file writer."""
|
||||
should_write_image = config.save_last_frame or (
|
||||
config.write_to_movie and not self.file_writer.num_plays
|
||||
)
|
||||
if self.file_writer.num_plays:
|
||||
self.file_writer.finish()
|
||||
# otherwise no animations were played
|
||||
elif config.write_to_movie or config.save_last_frame:
|
||||
if should_write_image:
|
||||
self.render_state(write_frame=False)
|
||||
# FIXME: for some reason the OpenGLRenderer does not give out the
|
||||
# correct frame values here
|
||||
|
|
@ -240,10 +246,11 @@ class Manager(Generic[SceneT]):
|
|||
"you can interact with the scene. "
|
||||
"Press `command + q` or `esc` to quit"
|
||||
)
|
||||
# TODO: Replace with actual dt instead
|
||||
# of hardcoded dt
|
||||
dt = 1 / config.frame_rate
|
||||
last_time = time.perf_counter()
|
||||
while not self.window.is_closing:
|
||||
current_time = time.perf_counter()
|
||||
dt = current_time - last_time
|
||||
last_time = current_time
|
||||
self._update_frame(dt)
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
|
@ -266,7 +273,9 @@ class Manager(Generic[SceneT]):
|
|||
# Animation Pipeline #
|
||||
# ----------------------------------#
|
||||
|
||||
def _update_frame(self, dt: float, *, write_frame: bool | None = None) -> None:
|
||||
def _update_frame(
|
||||
self, dt: float, *, write_frame: bool | None = None, run_updaters: bool = True
|
||||
) -> None:
|
||||
"""Update the current frame by ``dt``
|
||||
|
||||
Parameters
|
||||
|
|
@ -276,7 +285,8 @@ class Manager(Generic[SceneT]):
|
|||
Default value checks :attr:`_write_files` to see if it should be written.
|
||||
"""
|
||||
self.time += dt
|
||||
self.scene._update_mobjects(dt)
|
||||
if run_updaters:
|
||||
self.scene._update_mobjects(dt)
|
||||
self.scene.time = self.time
|
||||
|
||||
if self.window is not None:
|
||||
|
|
@ -400,10 +410,17 @@ class Manager(Generic[SceneT]):
|
|||
progression.shape[0],
|
||||
f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}",
|
||||
) as progress:
|
||||
if self._skipping:
|
||||
self.scene._update_animations(animations, run_time, run_time)
|
||||
self._update_frame(run_time, run_updaters=False)
|
||||
return
|
||||
for t in progression:
|
||||
dt, last_t = t - last_t, t
|
||||
self.scene._update_animations(animations, t, dt)
|
||||
self._update_frame(dt)
|
||||
run_updaters = not self.scene.is_current_animation_frozen_frame(
|
||||
animations
|
||||
)
|
||||
self._update_frame(dt, run_updaters=run_updaters)
|
||||
for anim in animations:
|
||||
if isinstance(anim, Wait) and anim.stop_condition:
|
||||
if anim.stop_condition():
|
||||
|
|
|
|||
|
|
@ -600,7 +600,7 @@ class Arrow(Line):
|
|||
super().__init__(*args, buff=buff, stroke_width=stroke_width, **kwargs) # type: ignore[misc]
|
||||
# TODO, should this be affected when
|
||||
# Arrow.set_stroke is called?
|
||||
self.initial_stroke_width = self.stroke_width
|
||||
self.initial_stroke_width = stroke_width
|
||||
self.add_tip(tip_shape=tip_shape)
|
||||
self._set_stroke_width_from_length()
|
||||
|
||||
|
|
|
|||
|
|
@ -942,6 +942,22 @@ class Mobject:
|
|||
"dt" in inspect.signature(updater).parameters for updater in self.updaters
|
||||
)
|
||||
|
||||
@property
|
||||
def has_updaters(self) -> bool:
|
||||
"""Test if ``self`` has an updater.
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:`bool`
|
||||
``True`` if at least one updater is added, ``False`` otherwise.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:meth:`get_updaters`
|
||||
|
||||
"""
|
||||
return len(self.updaters) > 0
|
||||
|
||||
def get_updaters(self) -> list[Updater]:
|
||||
"""Return all updaters.
|
||||
|
||||
|
|
|
|||
|
|
@ -160,18 +160,18 @@ class VMobject(Mobject):
|
|||
self.submobjects: list[VMobject]
|
||||
|
||||
# TODO: Find where color overwrites are happening and remove the color doubling
|
||||
# if "color" in kwargs:
|
||||
# fill_color = kwargs["color"]
|
||||
# stroke_color = kwargs["color"]
|
||||
if "color" in kwargs:
|
||||
fill_color = kwargs["color"]
|
||||
stroke_color = kwargs["color"]
|
||||
if fill_color is not None:
|
||||
self.fill_color = ManimColor.parse(fill_color)
|
||||
if stroke_color is not None:
|
||||
self.stroke_color = ManimColor.parse(stroke_color)
|
||||
|
||||
if fill_opacity is not None:
|
||||
self.fill_color = self.fill_color.set_opacity(fill_opacity)
|
||||
self.fill_color = self.fill_color.opacity(fill_opacity)
|
||||
if stroke_opacity is not None:
|
||||
self.stroke_color = self.stroke_color.set_opacity(stroke_opacity)
|
||||
self.stroke_color = self.stroke_color.opacity(stroke_opacity)
|
||||
|
||||
def _assert_valid_submobjects(self, submobjects: Iterable[VMobject]) -> Self:
|
||||
return self._assert_valid_submobjects_internal(submobjects, VMobject)
|
||||
|
|
@ -320,11 +320,8 @@ class VMobject(Mobject):
|
|||
if family:
|
||||
for submobject in self.submobjects:
|
||||
submobject.set_fill(color, opacity, family)
|
||||
|
||||
if color is not None:
|
||||
self.fill_color = ManimColor.parse(color)
|
||||
if opacity is not None:
|
||||
self.fill_color = [c.opacity(opacity) for c in self.fill_color]
|
||||
array_name = "fill_rgbas"
|
||||
self.update_rgbas_array(array_name, color, opacity)
|
||||
return self
|
||||
|
||||
def set_stroke(
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ class OpenGLRenderer(Renderer, RendererProtocol):
|
|||
pixel_height if pixel_height is not None else config.pixel_height
|
||||
)
|
||||
self.samples = samples
|
||||
if background_opacity:
|
||||
if background_opacity is not None:
|
||||
background_color = background_color.opacity(background_opacity)
|
||||
self.background_color = background_color.to_rgba()
|
||||
self.background_image = background_image
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ void main()
|
|||
discard;
|
||||
|
||||
float previous_index =
|
||||
texture2D(stencil_texture, vec2(gl_FragCoord.x / pixel_shape.x, gl_FragCoord.y / pixel_shape.y)).r;
|
||||
texture(stencil_texture, vec2(gl_FragCoord.x / pixel_shape.x, gl_FragCoord.y / pixel_shape.y)).r;
|
||||
|
||||
// Check if we are behind another fill and if yes discard the current fragment
|
||||
if (previous_index > index)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ void main()
|
|||
gl_FragDepth = gl_FragCoord.z;
|
||||
// Get the previous index that was written to this fragment
|
||||
float previous_index =
|
||||
texture2D(stencil_texture, vec2(gl_FragCoord.x / pixel_shape.x, gl_FragCoord.y / pixel_shape.y)).r;
|
||||
texture(stencil_texture, vec2(gl_FragCoord.x / pixel_shape.x, gl_FragCoord.y / pixel_shape.y)).r;
|
||||
// If the index is the same that means we are overlapping with the fill and
|
||||
// crossing through so we push the stroke forward a tiny bit
|
||||
if (previous_index < index && previous_index != 0)
|
||||
|
|
|
|||
|
|
@ -8,5 +8,5 @@ out vec4 frag_color;
|
|||
void main()
|
||||
{
|
||||
frag_color = texture(tex, f_uv);
|
||||
frag_color.a = 1.0;
|
||||
// frag_color.a = 1.0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ if TYPE_CHECKING:
|
|||
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
from manim.manager import Manager
|
||||
from manim.typing import Point3D, Vector3D
|
||||
from manim.typing import Point3D, StrPath, Vector3D
|
||||
|
||||
# TODO: these keybindings should be made configurable
|
||||
|
||||
|
|
@ -188,8 +188,10 @@ class Scene:
|
|||
"""
|
||||
# always rerender by returning True
|
||||
# TODO: Apply caching here
|
||||
return self.always_update_mobjects or any(
|
||||
mob.has_updaters for mob in self.mobjects
|
||||
return (
|
||||
self.always_update_mobjects
|
||||
or (len(self.updaters) > 0)
|
||||
or any(mob.has_updaters for mob in self.mobjects)
|
||||
)
|
||||
|
||||
def is_current_animation_frozen_frame(
|
||||
|
|
@ -197,7 +199,18 @@ class Scene:
|
|||
) -> bool:
|
||||
if len(animations) == 0:
|
||||
return False
|
||||
return all(getattr(anim, "is_static_wait", False) for anim in animations)
|
||||
|
||||
# Check if all animations are frozen frames
|
||||
any_frozen_frame = any(
|
||||
getattr(anim, "is_static_wait", False) for anim in animations
|
||||
)
|
||||
all_frozen_frame = all(
|
||||
getattr(anim, "is_static_wait", False) for anim in animations
|
||||
)
|
||||
if any_frozen_frame and not all_frozen_frame:
|
||||
raise ValueError("All animations must be frozen frames to be frozen frames")
|
||||
|
||||
return all_frozen_frame
|
||||
|
||||
def has_time_based_updaters(self) -> bool:
|
||||
return any(
|
||||
|
|
@ -469,14 +482,17 @@ class Scene:
|
|||
|
||||
def add_sound(
|
||||
self,
|
||||
sound_file: str,
|
||||
sound_file: StrPath,
|
||||
time_offset: float = 0,
|
||||
gain: float | None = None,
|
||||
gain_to_background: float | None = None,
|
||||
):
|
||||
raise NotImplementedError("TODO")
|
||||
if self.manager.file_writer is None:
|
||||
return
|
||||
time = self.time + time_offset
|
||||
self.file_writer.add_sound(sound_file, time, gain, gain_to_background)
|
||||
self.manager.file_writer.add_sound(
|
||||
sound_file, time, gain, gain_to_background=gain_to_background
|
||||
)
|
||||
|
||||
def get_state(self) -> SceneState:
|
||||
return SceneState(self)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import av
|
|||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import manim
|
||||
from manim import DR, Circle, Create, Manager, Scene, Star
|
||||
from manim.file_writer.file_writer import to_av_frame_rate
|
||||
from manim.utils.commands import capture, get_video_metadata
|
||||
|
|
@ -37,11 +38,12 @@ class StarScene(Scene):
|
|||
def test_gif_writing(tmp_path, config, write_to_movie, transparent):
|
||||
output_filename = f"gif_{'transparent' if transparent else 'opaque'}"
|
||||
config.media_dir = tmp_path
|
||||
config.quality = "low_quality"
|
||||
config.format = "gif"
|
||||
config.transparent = transparent
|
||||
config.output_file = output_filename
|
||||
Manager(StarScene).render()
|
||||
with manim.tempconfig({"renderer": "opengl"}):
|
||||
config.quality = "low_quality"
|
||||
config.format = "gif"
|
||||
config.transparent = transparent
|
||||
config.output_file = output_filename
|
||||
Manager(StarScene).render()
|
||||
|
||||
video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.gif"
|
||||
assert video_path.exists()
|
||||
|
|
@ -97,13 +99,14 @@ def test_codecs(
|
|||
codec,
|
||||
pixel_format,
|
||||
):
|
||||
output_filename = f"codec_{format}_{'transparent' if transparent else 'opaque'}"
|
||||
config.media_dir = tmp_path
|
||||
config.quality = "low_quality"
|
||||
config.format = format
|
||||
config.transparent = transparent
|
||||
config.output_file = output_filename
|
||||
Manager(StarScene).render()
|
||||
with manim.tempconfig({"renderer": "opengl"}):
|
||||
output_filename = f"codec_{format}_{'transparent' if transparent else 'opaque'}"
|
||||
config.media_dir = tmp_path
|
||||
config.quality = "low_quality"
|
||||
config.format = format
|
||||
config.transparent = transparent
|
||||
config.output_file = output_filename
|
||||
Manager(StarScene).render()
|
||||
|
||||
video_path = tmp_path / "videos" / "480p15" / f"{output_filename}.{format}"
|
||||
assert video_path.exists()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def test_t_values(config, using_temp_config, disabling_caching, frame_rate):
|
|||
def test_t_values_with_skip_animations(using_temp_config, disabling_caching):
|
||||
"""Test the behaviour of scene.skip_animations"""
|
||||
manager = Manager(SquareToCircle)
|
||||
manager._skip_animations = True
|
||||
manager._skipping = True
|
||||
scene = manager.scene
|
||||
scene._update_animations = Mock()
|
||||
manager.render()
|
||||
|
|
@ -100,13 +100,16 @@ def test_wait_with_stop_condition(using_temp_config, disabling_caching):
|
|||
|
||||
|
||||
def test_frozen_frame(using_temp_config, disabling_caching):
|
||||
scene = Manager(SceneForFrozenFrameTests)
|
||||
scene.render()
|
||||
assert scene.mobject_update_count == 0
|
||||
assert scene.scene_update_count == 0
|
||||
manager = Manager(SceneForFrozenFrameTests)
|
||||
manager.render()
|
||||
assert manager.scene.mobject_update_count == 0
|
||||
assert manager.scene.scene_update_count == 0
|
||||
|
||||
|
||||
def test_t_values_with_cached_data(using_temp_config):
|
||||
pytest.fail(
|
||||
"TODO: Implement `_write_hashed_movie_file` and partial movie file logic in `Manager.py` to support caching. Currently, caching logic is stubbed out or incomplete."
|
||||
)
|
||||
"""Test the proper generation and use of the t values when an animation is cached."""
|
||||
scene = SceneWithMultipleCalls()
|
||||
# Mocking the file_writer will skip all the writing process.
|
||||
|
|
@ -123,10 +126,10 @@ def test_t_values_with_cached_data(using_temp_config):
|
|||
def test_t_values_save_last_frame(config, using_temp_config):
|
||||
"""Test that there is only one t value handled when only saving the last frame"""
|
||||
config.save_last_frame = True
|
||||
scene = SquareToCircle()
|
||||
scene.update_to_time = Mock()
|
||||
scene.render()
|
||||
scene.update_to_time.assert_called_once_with(1)
|
||||
manager = Manager(SquareToCircle)
|
||||
scene = manager.scene
|
||||
manager.render()
|
||||
assert scene.time == 1
|
||||
|
||||
|
||||
def test_animate_with_changed_custom_attribute(using_temp_config):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue