快轉到主要內容

Manim - 用 Python 做出漂亮的數學動畫

Manim 是一套做數學動畫影片的 Python 函式庫。或許你在 Youtube 看過大佬 3Blue1Brown (Grant Sanderson) 的精美影片:這就是他開發,後來社群 fork 出來維護的

只要你會一點程式,就能簡單做出非常漂亮的影片。我會以程式範例 + 影片結果做 Manim 入門介紹。下面是我簡陋的成果,不過強烈建議去 Manim 官方網站3Blue1Brown 的頻道看看究竟可以做得多精美


  • Manim 是一個 python 函式庫
  • 是用程式碼當劇本,不是在圖形介面上拖拉元件
  • 你用 Manim 附的指令把劇本轉成影片

一個用 Manim 做動畫的程式長得像這樣

### demo.py
from manim import *

DEMO_POINTS_1 = [
  (0.8, 4.3), (2.4, 9.7), (3.1, 2.2), (4.1, 5.1), (5.7, 1.5),
  (6.3, 8.9), (7.5, 3.4), (8.1, 7.2), (9.8, 6.4), (10.2, 10.1)
]


class ProblemIllustration(Scene):
  def construct(self):
    ax = Axes(
      x_range=[-1, 11, 1],
      y_range=[-2, 11, 1],
      axis_config={"include_ticks": False, "color": GRAY_D},
      tips=True
    )

    dots = [
      Dot(ax.coords_to_point(x, y), radius=DEFAULT_DOT_RADIUS*1.5)
      for x, y in DEMO_POINTS_1
    ]

    # Show points in a plane
    self.add(ax)
    self.play(FadeIn(*dots, runtime=1.0))

from manim import * 就是使用這個函式庫,然後你定義「場景」,也就是 Scene 的子類別。所有影片內容都在 construct() 這個方法裡實作

Manim 有幾種物件,例如實體(演員)跟動畫(怎麼表演

  • Axes (座標軸) 跟 Dot (點) 是實體,創造時決定演員一開始的長相、位置等等
  • FadeIn (淡入) 是動畫,可以把實體的物件給他:FadeIn 這個物件就代表著「這個實體應該怎麼表演」這個敘述

光是創造物件還不會演出;那只是劇本、指示「有什麼」而已。必須把他們加入場景之中才會動起來

self.add(實體物件)
self.play(動畫物件)

最後再用 manim 的指令把劇本變成影片

$ manim -p demo.py ProblemIllustration

就會出現下面的影片。注意到座標軸是 add 上去,所以影片一開始就出現。而十個點是包在 FadeIn 動畫裡面再以 play 加進去,所以有淡入的效果


為什麼要用 Manim, 要用程式碼寫劇本控制動畫,而不是用其他影片製作軟體拖拉的方式?

因為用程式可以很準確、不用自己換算座標、有幾百個物件也可以幾行程式碼解決

例如要畫三角函數 Sin 曲線、一元二次方程式的圖、坐標系上面的點線…,用 Manim 能非常準確地畫出來,而你只要寫數學式就好

所以反過來,他並不是漫畫、電影那種影片。他是為了數學: Math animation


實體物件創造出來以後,仍能改變他們的屬性,例如要他位置往上移 .shift() ,或是形狀剛好包圍著另外的實體 .surround() 。利用該物件的一些 method, 還可以 chain 起來

    # Position text and show
    problem_text = MarkupText("4↗  <span foreground='yellow'>or</span>  4↘")
    problem_text.shift(UP * 3.2)
    self.play(Write(problem_text))
    self.wait(1.5)
    self.play(Unwrite(problem_text))

    # Highlight some points
    highlight_points = [
      Circle(color=YELLOW).surround(dots[i])
      for i in [2, 3, 7, 9]
    ]
    self.play(*[Create(hp) for hp in highlight_points], run_time=0.5)
    self.wait()
    self.remove(*highlight_points)
    self.wait()

要注意,改變屬性並不是動畫,只是補充說明這個實體應該長怎麼樣。例如雖然用了 .shift(),影片並不會看到文字出現後往上跑的動畫,而是一開始就出現在上方

Write / UnwriteCreate 都是動畫類別:你會看到文字寫出來的過程畫圓圈的過程。注意 Create 並不是創造物件,而是把「畫出來」這件事作成動畫

當你 play 了一個實體物件的動畫時,顯然你也把它加進場景了,所以動畫完畢還是看得到。FadeOut 淡出之類的「動畫」能把物件拿掉,不過如果要直接拿掉的話可以用 .remove()

題外話場景的 .add(), .remove() 是瞬間的指示不是動畫;如果是本來程式的最後一步,必須在後面多加 .wait(),也就是讓場景等一秒,攝影機繼續拍才喊卡,才能在影片看到效果


Manim 內建了非常多的動畫 class,抽象掉了很多內部細節操作(你能想像 Write 描邊+填色 把文字由左到右慢慢寫出來 要怎麼做嗎?),不過也有另一種動畫的方式是:操作實體物件的屬性

    # ...

    dots = [
      Dot(ax.coords_to_point(x, y), radius=DEFAULT_DOT_RADIUS*1.5)
      for x, y in DEMO_POINTS_1
    ]

    # ...

    dots_move = [
      dots[i].animate.move_to(
        ax.coords_to_point(i+1, 10-i)
      )
      for i in range(len(dots))
    ]
    self.play(*dots_move)

前面說過,改變實體物件的屬性不是動畫。所以這用到了實體物件的 .animate

例如實體物件本來就有 .move_to() 「移動位置」的方法,那在實體物件後先接 .animate 後面也可以接 .move_to(),表示要把「移動位置」用動畫呈現出來 – 其實 Manim 的動畫就是把兩個實體物件「內插」(interpolate) 的過程

.animate 做動畫的優缺點

  • 優點:如果 Manim 沒提供你想要的動畫實作
  • 優點:利用 ValueTracker + .add_updater() 同步控制多個物件的動畫
  • 缺點:會有你意料之外的效果

以官方的例子

class Scene4(Scene):
  def construct(self):
    square_1 = Square(color=BLUE).shift(2 * LEFT)
    square_2 = Square(color=GREEN).shift(2 * RIGHT)
    self.play(
      square_1.animate.rotate(PI),
      Rotate(square_2, angle=PI), run_time=2
    )
    self.play(
      square_1.animate.rotate(PI/2),
      Rotate(square_2, angle=PI/2), run_time=2
    )
    self.wait()

看程式,兩個正方形都是旋轉 180 度 (PI),但左邊的是縮小再放大。

因為動畫是把起點到終點的物件形狀先算出來,然後中間用內插的方式,看看形狀每個點從頭到尾怎麼走,畫出來。正方形轉 180 度後看起來還是一樣,只不過「點」都跑到對面,例如在左上做個記號,旋轉後會到右下。

如果沒有指定好中間怎麼走,內插就以為是直線跑:效果看起來像四個角在中間集合後繼續跑到對面,這就是 .animate

Rotate 這個動畫類別,實作有考慮每個點的移動路徑,所以最後效果看起來形狀大小都沒有變(同理接下來轉 90 度時,你仔細觀察兩個正方形在途中的大小)


因為動畫是兩個實體物件內插,所以也可以做這種酷炫的變形

class Scene5(Scene):
  def construct(self):
    square = Square(color=RED, fill_opacity=1)
    circle = Circle(color=GREEN, fill_opacity=0.2)
    triangle = Triangle(color=BLUE, fill_opacity=0.5)
    self.play(ReplacementTransform(square, circle))
    self.play(ReplacementTransform(circle, triangle))

官方文件上有更多更酷更實用的例子: https://docs.manim.community/en/stable/examples.html , 上面只是我隨便玩的

總之 Manim 的官方網站與文件寫得滿不錯,感覺得出社群很熱心,有興趣的話一定要看看 https://www.manim.community/


The videos in this page are made using Manim: The Manim Community Developers. (2023). Manim – Mathematical Animation Framework (Version v0.18.0) [Computer software]. https://www.manim.community/

若您覺得有趣, 請 追蹤我的Facebook 或  Linkedin, 讓你獲得更多資訊!