🎞️ Изучаем manim. Часть 1: основные сведения о библиотеке для новичков

Manim – библиотека Python для создания математических анимаций. Вы наверняка видели ролики канала 3Blue1Brown, автор которого и написал ее первую версию. Мы же будем изучать созданный сообществом разработчиков форк – manim community.

Установка библиотеки

Вариантов установки масса, но мы рассмотрим установку на локальную машину с операционной системой Windows. Другие варианты вы можете найти в документации. Для нормального функционирования библиотеки необходим ffmpeg. Прямая ссылка для загрузки: https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z.

Архив необходимо распаковать в любое место на диске и внести директорию библиотеки в переменную среды Path. Чтобы это сделать, необходимо зайти в свойства моего компьютера, выбрать дополнительные параметры системы, нажать кнопку переменные среды и добавить в переменную Path путь до папки ffmpeg/bin:

Далее необходимо инсталлировать manim. Вы можете установить библиотеку в основной системе или использовать виртуальные окружения. Пойдем вторым путем.

  • Создадим папку для нашего будущего проекта manim_ex.
  • Откроем консоль в выбранной папке и вызовем команду: python -m venv env. Это заклинание создаст виртуальное окружение с именем env.
  • Активируем окружение командой: env\Scripts\activate.bat.
  • Установим саму библиотеку: pip install manim.
  • Не закрывайте консоль, она нам еще пригодится.
Необходимый минимум программ инсталлирован, но для полноценной работы потребуется любой дистрибутив LaTeX. Создатели библиотеки рекомендуют использовать MikTeX из-за простоты добавления дополнительных пакетов. Скачиваем и устанавливаем его, не заботясь о выборе. Когда manim будет обращаться к MikTeX и не найдет нужного пакета, встроенный менеджер предложит его скачать.

На этом этапе мы готовы начать знакомится с библиотекой.

Hello world

Manim предлагает пользователю три концепции:

  • mobject (mathematical object)
  • animations
  • scene
Эти концепции реализованы в соответствующих классах: Mobject, Animation, Scene. Прежде, чем углубляться в их структуру, сделаем проект hello-world.

В папке manim-ex создадим файл hello.py и добавим в него следующие строки:

hello.py
from manim import *
 
 
class SquareToStar(Scene):
    def construct(self):
        square = Square()
        square.set_fill(GREEN, opacity=0.5)
        self.play(Create(square))

Далее возвращаемся в оставленную открытой консоль и выполняем команду:

manim -pql .\hello.py SquareToStar

Начнется процесс рендеринга, в результате которого автоматически откроется окно с похожей анимацией.

Разберемся построчно, что происходит.

from manim import *

Тут мы импортируем все, что содержится в библиотеке в глобальное пространство имен. В общем случае это будет плохой идеей, но разработчики библиотеки рекомендуют именно такой путь импорта, а не поименное указание необходимых функций, классов, констант. Ведь для нашего hello world мы импортировали уже четыре имени: Scene, Square, Green, Create.

class SquareToStar(Scene):
    def construct(self):
    ...

В этой конструкции мы наследуем от класса Scene, в котором нужно перегрузить функцию construct. В ней и происходит сборка сцены: добавление объектов, настройка их дизайна и расположения, настройка анимации.

square = Square()
square.set_fill(GREEN, opacity=0.5)
self.play(Create(square))

В этих строках создается объект Square (он же квадрат) и устанавливается зеленый цвет заливки с прозрачностью 0.5. В последней строке внутри метода self.play() создается анимация Create, которая постепенно проявляет объект на сцене.

Усложним пример, сделав трансформацию созданного нами зеленого квадрата в пятиконечную звезду с синим контуром и без фона, которая после исчезнет. Для этого используем следующий код:

hello.py
from manim import *
 
 
class SquareToStar(Scene):
    def construct(self):
        square = Square()
        square.set_fill(GREEN, opacity=0.5)
        star = Star(outer_radius=2, color = BLUE)
        self.play(Create(square)) 
        self.play(Transform(square, star))
        self.play(FadeOut(square))

Не забываем вызвать команду:

manim -pql .\hello.py SquareToStar

В результате получаем следующую анимацию:

Структура библиотеки и проектов анимации

После запуска последнего заклинания в папку с проектом добавилась директория media. Внутри нее лежат отрендеренные части нашей сцены и собранный итоговый файл. Найти его можно в каталоге media\videos\hello, где будут созданы папки, соответствующие выбранному качеству рендеринга.

Основным строительным блоком любой сцены manim является mobject.

Такие элементы, как Circle, Axis, Rectangle, Arrow и Star, являются mobject. MObject – не имеющий визуального представления абстрактный класс, от которого наследуются все указанные выше фигуры. При этом стоит обратить внимание, что все используемые manim визуальные объекты являются векторными. Для простоты понимания пока опустим этот момент. Будем всегда использовать понятие mobject, хотя фактически речь идет о его векторном варианте.

Расположение объектов

Чтобы отобразить mobject на экране, нужно передать его в метод add нашей сцены. Создадим сцену, где звезда будет видима одну секунду, а после исчезнет. Код даже не нуждается в комментариях. В новый файл mobjects.py поместим следующее:

mobjects.py
from manim import *
 
class CreatingMObjects(Scene):
    def construct(self):
        star = Star()
        self.add(star)
        self.wait(1)
        self.remove(star)
        self.wait()

Добавим в этот же файл еще одну сцену:

mobjects.py
class Shapes(Scene):
    def construct(self):
        star = Star()
        square = Square()
        circle = Circle()
 
        circle.shift(LEFT)
        star.shift(UP)
        square.shift(RIGHT)
 
        self.add(star, square, circle)
        self.wait(1)

Результат:

По умолчанию, объекты добавляются в начало координат сцены, расположенное в ее центре. Для изменения положения объектов, manim поддерживает много различных способов. Мы использовали новый метод любого mobjectshift(vector). Он сдвигает объект на одну стандартную единицу в сторону указанного вектора или суммы векторов. В нашей сцене созданы объекты звезды, квадрата и круг. Звезда была сдвинута влево на одну единицу, квадрат – вправо, круг – влево. Константы UP, RIGHT, LEFT входят в состав библиотеки manim и представляют собой numpy ndarray. К примеру, UP выглядит следующим образом:

UP: np.ndarray = np.array((0.0, 1.0, 0.0))

Рассмотрим другие способы, позволяющие изменять положение объектов на сцене. Это методы mobject: move_to(), align_to(), next_to().

Добавим новую сцену в файл mobjects.py. Создадим на ней те же объекты, что и в предыдущей: звезду, окружность и квадрат:

mobjects.py
class ShapesPlacement(Scene):
    def construct(self):
        star = Star()
        square = Square()
        circle = Circle()
 
        star.move_to(LEFT * 2)  # сдвинуть звезду на две единицы влево
        square.next_to(star, LEFT)  # поставить квадрат слева от звезды
        circle.align_to(star, LEFT)  # выровнять левую границу круга с левой границей звезды
        self.add(star, square, circle)
        self.wait(1)

Результат работы следующий:

Метод move_to() использует абсолютные координаты, т.е. относительно начала координат. next_to() использует координаты относительно объекта, переданного ему первым параметром. align_to() использует левую границу объектов для выравнивания по нужной стороне. Для расчетов положения используется квадрат, описанный вокруг любого объекта сцены. Его не видно, но это фишка движка рендеринга библиотеки.

Стиль и порядок отображения объектов

Чтобы изменить стиль объекта, используются следующие методы: set_stroke(), set_fill() и set_color().

Создадим еще одну сцену ShapesStyle со следующим содержимым:

mobjects.py
class ShapesStyle(Scene):
    def construct(self):
        star = Star().shift(LEFT)
        square = Square().shift(RIGHT)
        circle = Circle().shift(UP)
 
        star.set_color(RED)
        square.set_fill(color=YELLOW, opacity=0.5)
        circle.set_stroke(PINK, width=20)
 
        self.add(star, square, circle)
        self.wait(1)

Обратите внимание, что объекты manim поддерживают chaining.

star = Star().shift(LEFT)
  • set_stroke() используется для установки рамки заданной толщины и цвета.
  • set_color() изменяет цвет обводки фигуры.
  • set_fill() заполняет фигуру выбранным цветом с заданной прозрачностью в диапазоне от 0.0 (полностью прозрачный) до 1.0 (полностью непрозрачный).

Как бы это странно ни выглядело, но в manim три оси, а не две. Третья ось, которая смотрит на наблюдателя перпендикулярно плоскости монитора, отвечает за порядок отображения объектов на сцене. Чтобы изменить его, достаточно поменять порядок передачи объектов в метод сцены add(). Изменим в предыдущем коде предпоследнюю строку на следующую:

self.add(circle, star, square)

В результате последний объект списка оказался на самом верху. У нас это квадрат.

Анимации

Наконец мы с вами добрались до самого сочного, что есть в этой библиотеке – до анимаций.

Чтобы добавить анимацию, необходимо вызвать метод сцены play(). Создадим новый файл manim_animations.py и добавим в него следующий код:

manim_animations.py
from manim import *
 
class ShapeAnimation(Scene):
    def construct(self):
        square = Square()
        self.add(square)
        self.play(FadeIn(square))
        self.play(Rotate(square, PI/4))
        self.play(FadeOut(square))
        self.wait(1)

Результат будет следующим:

Разберемся по строкам, что здесь происходит:

  1. Создаем объект квадрата.
  2. Добавляем его в начало координат сцены.
  3. Проигрываем анимацию FadeIn (появление объекта).
  4. Проигрываем анимацию Rotate (поворот объекта) на угол PI/4.
  5. Проигрываем анимацию FadeOut (исчезновение объекта).
  6. Ждем одну секунду.

FadeIn и FadeOut плавно изменяют прозрачность объекта от 0 до 1 и 1 до 0 соответственно.

Кроме того, любое свойство mobject, которое может быть изменено, может быть и анимировано с помощью метода animate(). Создадим новую сцену, чтобы это продемонстрировать. Добавим квадрат красного цвета, изменим его цвет и сместим с поворот на одну единицу вверх.

manim_animations.py
class ShapePropsAnimation(Scene):
    def construct(self):
        square = Square().set_fill(BLUE, opacity=1.0)
        self.add(square)
 
        self.play(square.animate.set_fill(RED))
        self.wait(1)
 
        self.play(square.animate.shift(UP).rotate(PI/3))
        self.wait(1)

Результат:

  • animate() – это свойство любого mobject, которое анимирует все, что идет за ним по цепочке. Например, чтобы анимировать свойство set_fill(WHITE), необходимо его вызвать следующим образом:
animate.set_fill(WHITE)

По умолчанию, все переданные в качестве аргументов в play() анимации проигрываются одну секунду. Чтобы изменить это время, необходимо задать параметр run_time. Создадим новую сцену, чтобы это продемонстрировать.

manim_animations.py
class ShapeRunTimeAnimation(Scene):
    def construct(self):
        square = Square()
        self.add(square)
        self.play(square.animate.shift(UP), run_time=3)
        self.wait()

Тут анимация движения квадрата длится не одну секунду, а три.

Хотя manim имеет множество встроенных анимаций, вам может их не хватить. Чтобы реализовать собственные, придется создать и собственного наследника класса Animation, в котором необходимо перегрузить метод interpolate_mobject(). Он получает параметр alpha, который начинается с 0 и изменяется на протяжении всей анимации. Manim предоставляет этот параметр в методе interpolate_mobject() на основе частоты кадров видео и времени воспроизведения анимации. Он изменяется 0 до 1 и скрывает под собой относительное время анимации. Т.е. 0 – начало анимации, 0.5 – середина, 1 – конец.

Для примера создадим анимацию преобразования одного числа в другое. В manim присутствует анимация этой трансформации, FadeTransform, но она преобразует одно число в другое через исчезновение первого и появление второго. Мы сделаем по-другому. Пусть наша трансформация будет выглядеть, как счетчик от стартового до конечного числа. Для этого создадим класс CountAnimation, который является наследником Animation.

Для полного понимания, рассмотрим подробнее параметр alpha.

Создадим анимацию преобразования числа 50 в число 100. Тогда alpha будет иметь следующие значения:

  • alpha = 0, тогда текущее значение равняется 50, т.е. стартовому.
  • alpha = 0.5, тогда текущее значение равняется 75.
  • alpha = 1, тогда текущее значение равняется 50, т.е. конечному.

Мы начинаем со стартового числа и добавляем только некоторую часть разницы между концом и началом, которая будет увеличиваться в соответствии с альфа-значением. Итак, логика вычисления числа для отображения на каждом шаге будет следующей:

50+alpha(10050)

Реализуем классы анимации и сцены. Создадим файл count_animation.py в папке с проектом:

count_animation.py
from manim import *
 
class CountAnimation(Animation):
    def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
        super().__init__(number, **kwargs)
        self.start = start
        self.end = end
   
    def interpolate_mobject(self, alpha: float) -> None:
        value = self.start + (alpha * (self.end - self.start))
        self.mobject.set_value(value)
 
 
class CustomCountingScene(Scene):
    def construct(self):
        number = DecimalNumber().set_color(WHITE).scale(5)
        number.add_updater(lambda number: number.move_to(ORIGIN))
 
        self.add(number)
        self.wait()
        self.play(CountAnimation(number, 0, 100), run_time=4, rate_func=linear)
        self.wait()

DecimalNumber – это еще один Mobject. Наша анимация принимает в свой конструктор объект number, и начальные и конечные значения от 0 до 100, преобразование будет занимать 4 секунды, а скорость преобразования будет линейной. В результате получим следующую анимацию:

Трансформации

Трансформация – это тоже анимация, которая преобразует один mobject в другой. В самом начале статьи мы с вами использовали трансформацию квадрата в звезду.

Для примера, создадим трансформацию квадрата в круг в новом файле transform.py со следующим содержимым:

transform.py
from manim import *
 
 
class SquareToCircleScene(Scene):
    def construct(self):
        square = Square().set_fill(BLUE, opacity=1.0)
        circle = Circle().set_fill(RED, opacity=1.0)
        self.add(square)
        self.play(Transform(square, circle), run_time=2)
        self.wait()

Результат:

Другие трансформации можно найти в документации.

***

В следующих статьях цикла мы рассмотрим более сложные аспекты использования библиотеки manim. Удачи в обучении!

Дополнительные материалы:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
08 октября 2017

13 ресурсов, чтобы выучить математику

Среди разработчиков часто возникают споры о том, необходимо ли изучать мате...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...