Search code examples
pythonmatplotlibanimationblitmatplotlib-animation

How to animate Poly3DCollection using FuncAnimation with blit=True?


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


Solution

  • 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)
        ...