2021/11/07

クールな数式アニメーションを作る方法

兎角、視覚に訴えるというのは実に効果的な手法だと思う。やり過ぎれば誇張、詐欺との誹りは免れられない一方、難解な分野に対するハードルを下げたり、人々が興味関心を持つきっかけに繋がったりもする。

とりわけ数学はその代表格と言える。数字の羅列から勝手に世界観を構築できる人間はとても稀有な存在だ。よくよく聞けば面白い話でも「よくよく」の姿勢になってもらうまでが難しい。世界中の教育者たちは生徒(これはなにも子供だけに限った話ではない)にいかにして興味を持たせるか、日々頭を悩ませていることだろう。

そこへ行くと今は良い時代だ。ほとんどの人が手のひらサイズのコンピュータを持っており、以前は困難だった動画や音声などのリッチコンテンツの配信も当たり前になった。その中でも特に3Blue1BrownというYoutubeチャンネルは、むしろ数学が苦手な人にほど観てもらいたい。滑らかで美しいアニメーションと、決して上っ面だけに留まらない精緻な解説はきっと数学への関心を高めてくれる。

例えば上記の動画は物理エンジンを利用したブロック同士の衝突シミュレーション。驚くべきことにその衝突回数はブロックの重さを増やすたびに、円周率の値へと近似していくらしい。なぜそうなるのか、高校レベルの運動方程式と三角関数を交えて解説している。いかにも物理と数学の共演ぶりが実感できてとてもテンションが上がる。

こういったすばらしいアニメーションを作成するにはさぞかし手間がかかるに違いない。操作を覚えるだけでも人生を使い果たしそうな3DCGソフトとか、どうせAdobeのなんとかかんとかみたいな名前のソフトを使っているんだろう、と僕も思っていた。

ところがわれわれはこれらのアニメーションをエディタ一つとPythonの知識だけで、なおかつ無料で作ることができる。 なぜなら他ならぬ3Blue1Brown自身が描画エンジンをオープンソースとして提供しているからだ。本エントリではこの描画エンジン「Manim」を簡単に紹介する。

凡例

導入方法はリポジトリのページで既に説明されているが、Arch Linux系のディストリの場合はpacman -S manimで入れる方がより手軽かもしれない。コードの内容によってはLaTeX関連のパッケージも必要になる。予めtexlive-coretexlive-binを導入しておくと面倒が少ない。

導入が済んだら適当な名前のディレクトリを拵えて直下にpythonファイルを作成する。作成したら、まずは下記のコードをコピペしよう。

1
2
UNKO/
└─scene.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from manim import *

class SquareToCircle(Scene):
    def construct(self):
        circle = Circle()
        square = Square()
        
        self.play(Create(square))
        self.play(Transform(square, circle))
        self.play(FadeOut(square))

保存後、コンソールでscene.py -p -qlを実行すると正方形が円に変わるアニメーションが再生されるはずだ。続いて、グラフを派手に表示させてみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from manim import *

class FollowingGraphCamera(MovingCameraScene):
    def construct(self):
        self.camera.frame.save_state()

        # create the axes and the curve
        ax = Axes(x_range=[-1, 10], y_range=[-1, 10])
        graph = ax.plot(lambda x: np.sin(x), color=BLUE, x_range=[0, 3 * PI])

        # create dots based on the graph
        moving_dot = Dot(ax.i2gp(graph.t_min, graph), color=ORANGE)
        dot_1 = Dot(ax.i2gp(graph.t_min, graph))
        dot_2 = Dot(ax.i2gp(graph.t_max, graph))

        self.add(ax, graph, dot_1, dot_2, moving_dot)
        self.play(self.camera.frame.animate.scale(0.5).move_to(moving_dot))

        def update_curve(mob):
            mob.move_to(moving_dot.get_center())

        self.camera.frame.add_updater(update_curve)
        self.play(MoveAlongPath(moving_dot, graph, rate_func=linear))
        self.camera.frame.remove_updater(update_curve)

        self.play(Restore(self.camera.frame))

カッチョいい。では次は三角関数だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from manim import *

class SineCurveUnitCircle(Scene):
    # contributed by heejin_park, https://infograph.tistory.com/230
    def construct(self):
        self.show_axis()
        self.show_circle()
        self.move_dot_and_draw_curve()
        self.wait()

    def show_axis(self):
        x_start = np.array([-6,0,0])
        x_end = np.array([6,0,0])

        y_start = np.array([-4,-2,0])
        y_end = np.array([-4,2,0])

        x_axis = Line(x_start, x_end)
        y_axis = Line(y_start, y_end)

        self.add(x_axis, y_axis)
        self.add_x_labels()

        self.origin_point = np.array([-4,0,0])
        self.curve_start = np.array([-3,0,0])

    def add_x_labels(self):
        x_labels = [
            MathTex("\pi"), MathTex("2 \pi"),
            MathTex("3 \pi"), MathTex("4 \pi"),
        ]

        for i in range(len(x_labels)):
            x_labels[i].next_to(np.array([-1 + 2*i, 0, 0]), DOWN)
            self.add(x_labels[i])

    def show_circle(self):
        circle = Circle(radius=1)
        circle.move_to(self.origin_point)
        self.add(circle)
        self.circle = circle

    def move_dot_and_draw_curve(self):
        orbit = self.circle
        origin_point = self.origin_point

        dot = Dot(radius=0.08, color=YELLOW)
        dot.move_to(orbit.point_from_proportion(0))
        self.t_offset = 0
        rate = 0.25

        def go_around_circle(mob, dt):
            self.t_offset += (dt * rate)
            # print(self.t_offset)
            mob.move_to(orbit.point_from_proportion(self.t_offset % 1))

        def get_line_to_circle():
            return Line(origin_point, dot.get_center(), color=BLUE)

        def get_line_to_curve():
            x = self.curve_start[0] + self.t_offset * 4
            y = dot.get_center()[1]
            return Line(dot.get_center(), np.array([x,y,0]), color=YELLOW_A, stroke_width=2 )


        self.curve = VGroup()
        self.curve.add(Line(self.curve_start,self.curve_start))
        def get_curve():
            last_line = self.curve[-1]
            x = self.curve_start[0] + self.t_offset * 4
            y = dot.get_center()[1]
            new_line = Line(last_line.get_end(),np.array([x,y,0]), color=YELLOW_D)
            self.curve.add(new_line)

            return self.curve

        dot.add_updater(go_around_circle)

        origin_to_circle_line = always_redraw(get_line_to_circle)
        dot_to_curve_line = always_redraw(get_line_to_curve)
        sine_curve_line = always_redraw(get_curve)

        self.add(dot)
        self.add(orbit, origin_to_circle_line, dot_to_curve_line, sine_curve_line)
        self.wait(8.5)

        dot.remove_updater(go_around_circle)

こうやって動いているのを見ると三角関数の実態も一段と掴みやすくなる。その上、gifにしてもあまり重くならない。動画コンテンツのみならず、このように文章の補助に用いる形でもかなりの効力を発揮してくれる。もし意欲のある教育者たちの間に広まったら、今後の教育コンテンツは大化けするかもしれない。

せっかくなのでうんこっぽい図形を出力できるか試してみたいが、僕の知識量ではまだ時間がかかりそうだ。先にできた人がいたらぜひ教えてほしい。

参考文献

Manim Community
Example Gallery

©2011 Rikuoh Tsujitani | Twitter | RSS | 小説