4

I'm trying to animate a rotating cube. For that I use Poly3DCollection and animate it using FuncAnimation:

anim = animation.FuncAnimation(fig, visualize_rotation, fargs=[collection],
                               init_func=partial(init_func, ax, collection),
                               frames=360, interval=1000 / 30)

But it renders each frame very slowly so that I get just a few frames per second. To fix it I tried to add parameter blit=True in the hope that it will improve rendering speed, but this way I cannot see the cube.

This is what I see in the window: enter image description here

Weirdly enough the cube is visible when saving the figure. This is the result I get: enter image description here

I made sure that visualize_rotation returns list of artists [collection] that is required by blit=True as noted in this question, but the cube is still not visible.

So, how can I use blit flag in this case, while being able to see the cube during the animation?

Full code:

import math
from functools import partial

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import animation
from mpl_toolkits.mplot3d.art3d import Poly3DCollection

def visualize_rotation(frame, collection):
    angle = math.radians(2) * frame

    points = np.array([[-1, -1, -1],
                       [1, -1, -1],
                       [1, 1, -1],
                       [-1, 1, -1],
                       [-1, -1, 1],
                       [1, -1, 1],
                       [1, 1, 1],
                       [-1, 1, 1]])

    Z = np.zeros((8, 3))
    for i in range(8):
        Z[i, :] = [
            math.cos(angle) * points[i, 0] - math.sin(angle) * points[i, 1],
            math.sin(angle) * points[i, 0] + math.cos(angle) * points[i, 1],
            points[i, 2]
        ]
    Z = 10.0 * Z

    # list of sides' polygons of figure
    vertices = [[Z[0], Z[1], Z[2], Z[3]],
                [Z[4], Z[5], Z[6], Z[7]],
                [Z[0], Z[1], Z[5], Z[4]],
                [Z[2], Z[3], Z[7], Z[6]],
                [Z[1], Z[2], Z[6], Z[5]],
                [Z[4], Z[7], Z[3], Z[0]]]

    # plot sides
    collection.set_verts(vertices)
    print(frame)

    return [collection]

def init_func(ax, collection):
    ax.set_xlim(-15, 15)
    ax.set_ylim(-15, 15)
    ax.set_zlim(-15, 15)

    ax.set_box_aspect(np.ptp([ax.get_xlim(), ax.get_ylim(), ax.get_zlim()], axis=1))

    return [collection]

def animate_rotation():

    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d', proj_type='persp')

    collection = Poly3DCollection([[np.zeros(3)]], facecolors='white',
                                  linewidths=1, edgecolors='r', alpha=0.8)
    ax.add_collection3d(collection)

    # noinspection PyUnusedLocal
    anim = animation.FuncAnimation(fig, visualize_rotation, fargs=[collection],
                                   init_func=partial(init_func, ax, collection),
                                   frames=360, interval=1000 / 30, blit=True)

    plt.show()

Edit:

I've added computation of frames per second and plotted it:

timestamps = []

def visualize_rotation(frame, collection):
    ...

    # plot sides
    collection.set_verts(vertices)
    global timestamps

    timestamps.append(time.time())
    print(round(1 / np.mean(np.diff(timestamps[-1000:])), 1))

    return [collection]

def animate_rotation():
    ...

    plt.plot(np.diff(timestamps))
    plt.ylim([0, 0.1])
    plt.show()

This is what happens when the window is in normal size and the drawing speed is slow (time in seconds vs frame number): missing frames

And this is the plot when the window is tiny: normal framerate

The start of the plot shows resizing of the window. Only 2 frames were dropped (at about 50 and 150) in the second case, and the overall frame rate is about 30 fps as desired. I'm looking for the same kind of behavior when the window is normally sized. When I turn blit on, the plot looks fine, but the problem is that the cube is not visible. enter image description here

fdermishin
  • 3,519
  • 3
  • 24
  • 45
  • [This is the rotation speed](https://imgur.com/a/QGRrdeS) without `blit=True` for me. Is yours significantly slower or do you ask to increase this speed? – Mr. T Feb 11 '21 at 20:32
  • Mine is just a bit slower. But it is noticeably slower than 30 fps that I aim for (`interval=1000 / 30`). One thing that I noticed is that the speed depends on the figure size. If the window is resized to a tiny one, then the animation speed is correct and the animation is nice and smooth. But for a default window size it is about 2 times slower. So, I ask to increase the speed. – fdermishin Feb 11 '21 at 20:46
  • In my environment, I could not reproduce the animation of the cube, but I think you can improve the speed by not using the initialization function and setting the interval value to the minimum value of 1. The speed at which `print(frame)` is displayed in the current code was also improved by feel. – r-beginners Feb 12 '21 at 01:18
  • 1
    @r-beginners When I set the the interval to 1, then `visualize_rotation` is called about 150 times a second, but most of the frames are being dropped. I'm not sure how the initialization function can affect the speed, because it is called only once. I've updated the question by adding plots of frame rate. – fdermishin Feb 12 '21 at 08:29
  • Calling `visualize_rotation` is not a bottleneck at all, because it takes 1 millisecond per frame on average, but the bottleneck is the rendering inside matplotlib – fdermishin Feb 12 '21 at 08:33
  • If the slow factor is the rendering inside matplotlib, it's hard to improve the speed. Here is the manual for matplotlib in [PDF format](https://matplotlib.org/3.3.3/contents.html). Please refer to the manual, which also mentions 'blitting'. (p219) – r-beginners Feb 12 '21 at 09:51
  • You can precompute the collections then it gets a little faster. – Jens Munk Feb 16 '21 at 18:23
  • I doubt it. All improvements I introduced to the code above to avoid loops and vectorize numpy functions did not increase the speed substantially. I stopped my attempts when I saw [this from ImportanceOfBeingEarnest](https://stackoverflow.com/a/45713451/8881141). – Mr. T Feb 16 '21 at 18:41

1 Answers1

4

I found a one-liner fix for you: add do_3d_projection after you update the vertices.

...
# plot sides
collection.set_verts(vertices)
collection.do_3d_projection(collection.axes.get_figure().canvas.get_renderer())
print(frame)

return [collection]

It's probably a bug that it's not being called in the underlying code when blit=True.

Also, another bug pops up; the last frame is somehow getting carried over when the animation repeats in blit=True mode. To fix this, add ax.clear() and ax.add_collection3d() in your init_func:

def init_func(ax, collection):
    ax.clear()
    ax.add_collection3d(collection)
    ax.set_xlim(-15, 15)
    ax.set_ylim(-15, 15)
    ...
Mark H
  • 4,246
  • 3
  • 12
  • 1
    Thank you! Adding `do_3d_projection` solved the issue. The code works without adding `ax.clear()` in my case, but the cube disappears for one frame each time the animation is repeated. Moving `ax.set_lim` and `ax.set_box_aspect` out of `init_func` fixed this (the cube doesn't disappear, however the frame is dropped) – fdermishin Feb 17 '21 at 07:00
  • You're welcome! I'm getting a lot higher fps with blit=True and I hope you are too. – Mark H Feb 17 '21 at 07:13
  • @MarkH the renderer arg is now deprecated and `do_3d_projection` didn't work for me: I have half drawn patches [here](https://twitter.com/ccaprani/status/1440998707894185984) - this link to twitter links to the YouTube render & source code in case you're interested. Cheers! – Colin Sep 23 '21 at 14:20
  • 1
    @Colin looks like you got your animations done at least. In the new code, the _proj_transform_vec operation in do_3d_projection has been changed in v3.4 to use self.axes.M instead of self.renderer.M (self in this case is the Poly3DCollection object), so what I would if I were to force a fix would be to make sure that the M attribute of the axes object within my Poly3DCollection matched the renderer object's and was correctly updated frame-to-frame (or you can hack it back and just change the underlying code back to using renderer.M - no idea if that would actually work though). – Mark H Sep 26 '21 at 21:28
  • @MarkH thanks for responding - I see that now in the code alright - very subtle! I found that the problem I had though is a bug with `set_verts` and I [raised an issue](https://github.com/matplotlib/matplotlib/issues/21163). Will see if anything comes of it - I don't know enough about it to help further unfort. – Colin Sep 28 '21 at 01:51