manim icon indicating copy to clipboard operation
manim copied to clipboard

AnimationGroup: optimized interpolate() and fixed alpha bug on finish()

Open chopan050 opened this issue 2 years ago • 2 comments

Overview: What does this pull request change?

  • In AnimationGroup.build_animations_with_timings(), the start times and end times of each animation are now calculated in a vectorized way: now AnimationGroup.anims_with_timings is a NumPy array with custom type (object, float, float), instead of a list of tuples. This allows for vectorization and fancy indexing in the next point.
  • AnimationGroup.interpolate() was vectorized thanks to the previous changes. Not only that: instead of updating all the animations, now only "ongoing" animations (those which have started and not yet finished) are updated. The ongoing animations are fancy-indexed from the now-ndarray anims_with_timings attribute. All of this results in a major speedup for AnimationGroup.interpolate().
  • There was a bug where, if you used a "reverse" rate_func which intended to make the submobjects reach an alpha == 0 state, when AnimationGroup.finish() gets called, it would call all of its subanimations' finish() method which would make all the submobjects be reset to an alpha == 1 state instead. To fix this, an AnimationGroup.interpolate(1) was added to the AnimationGroup.finish() method.

Motivation and Explanation: Why and how do your changes improve the library?

Consider this scene:

class ChaosGame(Scene):
    def construct(self):
        L = 8
        h = L * np.sqrt(3) / 2
        vertices = np.array([[0, h/2, 0], [-L/2, -h/2, 0], [L/2, -h/2, 0]])

        N = 1000
        points = np.empty((N, 3))
        choices = np.random.randint(3, size=N)
        colors = [RED, YELLOW, BLUE]
        points[-1] = [0, 0, 0]
        for i in range(N):
            points[i] = (points[i-1] + vertices[choices[i]]) / 2
        
        dots = [Dot(p, color=colors[ch], radius=0.02) for p, ch in zip(points, choices)]

        self.wait()
        self.play(AnimationGroup(*[FadeIn(dot) for dot in dots], lag_ratio=0.5, run_time=20))
        self.wait(5)

When an AnimationGroup has too many subanimations (like in this example), performance gets a hit. Therefore, I optimized AnimationGroup for that scenario.

This specific example was extreme, and so is its corresponding optimization: a speedup of almost 150x in AnimationGroup.interpolate()! Other scenarios may not benefit that much, but it's still a huge advancement.

Before After
bild bild

Reviewer Checklist

  • [ ] The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • [ ] If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • [ ] If applicable: newly added functions and classes are tested

chopan050 avatar Dec 20 '23 22:12 chopan050

I realized that my implementation creates a bug when there are too many subanimations, as self.ongoing_anim_bools and new_ongoing won't always intersect, and there might be animations in between which would be skipped by this.

I noticed that when running my ChaosGame with 5000 mobjects. :(

I'll make this a draft meanwhile, I hope that I can fix this soon.

@behackl, about your AnimationGroup implementation you talked about last night: maybe it would be a good idea to integrate those changes now that we're on it. May I pull your changes into my branch? Or is it possible that you could push them?

chopan050 avatar Dec 21 '23 13:12 chopan050

I fixed the problem and marked the PR as ready for review again. In the end I reverted my implementation to do the same thing you did, Benjamin: to compute when animations have started and finished, rather than being just "ongoing".

I also made AnimationGroup.update_mobjects() update only the ongoing animations (i.e. started and not finished), just like you did.


I tried to implement the other changes and see what happened. It didn't go well. :(

For my ChaosGame, the FadeIn animations expect their begin() method to be called in the beginning, because that's how they create the target and starting mobject, where the starting mobject is invisible at first. Not doing this right at the beginning (like what you do with AnimationGroup.begin(): it's just a pass) messes everything up, as all mobjects are immediately present on scene and they barely blink when their animation actually begins. (Now that i think of it, it sounds more of an AnimationGroup issue, as in "why are you adding all the Mobjects preemptively")

Implementing all those changes is gonna be more complex than it looks, and it definitely should be done in another PR ;)

chopan050 avatar Dec 21 '23 16:12 chopan050