slama.dev

Manim – OpenGL and Interactivity

In this addition to my Manim series, we’ll cover the (at the moment rather experimental) OpenGL backend for faster GPU-based rendering.

It is heavily based on aquabeam’s article and Benjamin Hackl’s video (criminally underrated YouTube channel, by the way) about ManimCommunity’s OpenGL support, so feel free to refer to them if you’d like to learn more.

Introduction

By default, Manim uses Cairo for all of its rendering, which mostly utilizes the CPU. This is usually fine, but quickly falls apart for more complex animations (especially the 3D ones) where it can take minutes or even hours to render a single animation.

To greatly speed things up, we can switch the backend to OpenGL, which uses the GPU and thus greatly improves the rendering times, to the point that it is now possible to use the scene interactively!

To start things of, let’s slightly edit one of the first scenes in the series:

from manim import *
from manim.opengl import *


class IntroScene(Scene):
    def construct(self):
        square = Square(color=RED).shift(LEFT * 2)
        circle = Circle(color=BLUE).shift(RIGHT * 2)

        self.play(Write(square), Write(circle))

        # moving objects
        self.play(
            square.animate.shift(UP * 0.5),
            circle.animate.shift(DOWN * 0.5)
        )

        # rotating and filling the square (opacity 80%)
        # scaling and filling the circle (opacity 80%)
        self.play(
            square.animate.rotate(PI / 2).set_fill(RED, 0.8),
            circle.animate.scale(2).set_fill(BLUE, 0.8),
        )

        # this is new!
        self.interactive_embed()

Saving the scene to a file, we can use

manim <file_name> -p --renderer=opengl

to render and preview the file with the OpenGL backend.

After running the command, there are a few things we can notice:

This does exactly what you think – we now have an interactive command prompt available which we can use to further animate and interact with the scene (try mouse drag!).

Let’s now issue some commands – we can run the following command in the prompt

self.play(Write(Text("This is awesome!")))

to write some text and

self.play(square.animate.set_color(BLUE), circle.animate.set_color(RED))

to swap the colors of the background objects. Pretty awesome, right?

We can of course use self.interactive_embed() as many times as we like – to exit the current interactive session and continue onto the next one, we can either type exit, or just Ctrl+D.

To render to file, we can additionally pass the --write_to_movie flag, which will render to file but disable interactivity, so if you want to record the interactive animations, I’d suggest to record the window/screen using software like OBS.

Mouse and Keyboard

Now this in and of itself is incredible (at least for a long-time user of Manim like me), but we can take this to a whole another level by introducing keyboard and mouse interactivity.

This comes in the form of on_key_{press/release} and on_mouse_{press/motion/scroll/drag} functions [source code], which we can override in our scene and achieve further interactivity.

For example, let’s create a simple scene that grows/shrinks an object based on key presses:

from manim import *
from manim.opengl import *

# Pyglet key constants
from pyglet.window import key


class KeyboardScene(Scene):
    def construct(self):
        # we're using self so it's available throughout the scene
        self.circle = Circle(color=BLUE)

        self.play(Write(self.circle))

        self.interactive_embed()

    def on_key_press(self, symbol, modifiers):
        """Called each time a key is pressed."""
        # grow the circle when plus is pressed
        if symbol == key.PLUS:
            self.play(self.circle.animate.scale(2))

        # shrink it when minus is pressed
        elif symbol == key.MINUS:
            self.play(self.circle.animate.scale(1 / 2))

        # so we can still use the default controls
        super().on_key_press(symbol, modifiers)

Once we get to interact with the scene, we can press + to grow the size of the circle on the screen and - to shrink it. The interactive window uses Pyglet, which is why we’re importing the pyglet.window.key constants.

We can do a similar thing for interactivity with the mouse:

from manim import *
from manim.opengl import *

from pyglet.window import key


class MouseScene(Scene):
    def construct(self):
        self.circle = Circle(color=BLUE)

        self.play(Write(self.circle))

        self.interactive_embed()

    def on_mouse_drag(self, point, d_point, buttons, modifiers):
        """Called each time the mouse is dragged (moves pressed across the windows)."""
        # resize the circle to where the mouse cursor currently is
        new_radius = np.linalg.norm(point)

        # no animations (the object is already in the scene), only changes!
        self.circle.become(
            Circle(
                color=BLUE,
                radius=new_radius,
                fill_opacity=0.5 * abs(np.sin(new_radius)),  # for some spark ;)
            )
        )

        # here we DON'T want to use the default controls since dragging moves the camera

Camera

The OpenGL camera [source code] offers quite a few functions that work really well in combination with interactivity and can be combined in a really nice way.

For example, one neat thing we can do is manually move camera into a number of positions and then smoothy interpolate among them using the following code:

from manim import *
from manim.opengl import *

from pyglet.window import key


class CameraScene(Scene):
    def construct(self):
        square = Square(color=RED).shift(LEFT * 2)
        circle = Circle(color=BLUE).shift(RIGHT * 2)

        self.play(Write(square), Write(circle))

        # moving objects
        self.play(
            square.animate.shift(UP * 0.5),
            circle.animate.shift(DOWN * 0.5)
        )

        # rotating and filling the square (opacity 80%)
        # scaling and filling the circle (opacity 80%)
        self.play(
            square.animate.rotate(PI / 2).set_fill(RED, 0.8),
            circle.animate.scale(2).set_fill(BLUE, 0.8),
        )

        self.camera_states = []

        self.interactive_embed()

    def on_key_press(self, symbol, modifiers):
        # + adds a new camera position to interpolate
        if symbol == key.PLUS:
            print("New position added!")
            self.camera_states.append(self.camera.copy())

        # P plays the animations, one by one
        elif symbol == key.P:
            print("Replaying!")
            for cam in self.camera_states:
                self.play(self.camera.animate.become(cam))

        super().on_key_press(symbol, modifiers)

Mobject → OpenGLMobject

Since Cairo and OpenGL’s Mobject implementations are incompatible, Manim uses magic to make classes like Square and Circle usable. This means that if you wish to use the base classes like Mobject, VMobject and Surface, you’ll have to use their OpenGL counterparts (OpenGLVMobject and OpenGLSurface respectively).

Since none of this is really documented, you’ll have to look at the source code.

Mobject is dead. Long live Mobject.

More examples!

Here are some more examples of what you can do with OpenGL, mostly to inspire rather then document (since you’ll have to do quite a bit of digging through the source code regardless).

This section is ever-expanding and will contain more examples as I experiment with OpenGL!

Point-cloud

from manim import *
from manim.opengl import *


class BunnyScene(Scene):
    def construct(self):
        pointcloud = OpenGLPMobject()

        # one side of the Stanford bunny
        # https://slama.dev/assets/manim/bunny.txt
        points = []
        with open("bunny.txt") as f:
            for line in f.read().splitlines():
                points.append(list(map(float, line.split())))

        pointcloud.add_points(points)

        # scale + color
        pointcloud.scale(20)
        pointcloud.set_color_by_gradient((RED, GREEN, BLUE))

        self.play(Create(pointcloud))

        self.interactive_embed()