3Blue1Brown 动画制作教程(7)--相见恨晚的线性变换

3Blue1Brown 动画制作教程(7)--相见恨晚的线性变换

相见恨晚的线性变换


我已经忍不住要写下本文了,可以说是“期待已久”!

本文内容难度较大,如果是第一次接触 Manim 而且很感兴趣,还请按照教程顺序从头开始学习。

我看的第一个 3Blue1Brown 动画是《线性代数的本质》,当时的心情可以用震撼来形容。我花了很多时间,终于找到了线性变换的常用场景类—— LinearTransformationScene 类及其使用的基本方法,应用该场景类在制作动画的时候可以起到事半功倍的效果。

LinearTransformationScene 类的定义位于 ~\manim\manimlib\scene\vector_space_scene.py 文件中,其继承于 VectorScene 类,而VectorScene 类又继承于场景基类—— Scene 类。

现在实例化以下名为 FirstLinearTransformation 的类:

class FirstLinearTransformation(LinearTransformationScene):
    def construct(self):
        mob = Circle(color=PURPLE)
        mob.move_to(RIGHT+UP*2)
        vector_array1 = np.array([[1], [2]])
        vector_array2 = np.array([[2], [1]])
        matrix = [[2, 0], [1, 1]]
        
        self.add_transformable_mobject(mob)
        self.add_vector(vector_array1,color=YELLOW)
        self.add_vector(vector_array2,color=BLUE)
        self.apply_matrix(matrix)
        self.wait(3)

将得到以下动画:

可以看出,这个动画中的平面包含两层,上层可以被旋转、拉升、压缩;下层则作为原始状态的参考平面,固定不动。

该动画描述的是平面所有向量依据矩阵 \left[ {\begin{array}{*{20}{c}}2&0\\1&1\end{array}} \right] 进行线性变换,从中还可以看出来:平面中原本平行的线在经过线性变换之后依然平行,且原点位置不变,这两点是线性变换的基本性质。另外,线性变换矩阵 \left[ {\begin{array}{*{20}{c}}2&0\\1&1\end{array}} \right] 的第一列 \left[ {\begin{array}{*{20}{c}}2\\1\end{array}} \right] 即为横轴的单位向量 \left[ {\begin{array}{*{20}{c}}1\\0\end{array}} \right] 经过线性变换后得到的向量,第二列 \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right] 即为纵轴的单位向量 \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right] 经过线性变换后得到的向量。

代码中 add_transformable_mobject() 方法可以添加一个可与平面同时进行线性变换的图形,这样可以看到图形在线性变换中的变形过程。

apply_matrix() 方法通过输入一个二维矩阵,使得整个平面依据该矩阵进行线性变换。

为了更加直观,可以在平面上添加一些刻度,并给出平面中的四个矢量的线性变换计算式,这四个矢量分别是横纵坐标的单位向量 (1,0)(0,1) ,已经另外添加的两个向量 (1,2)(2,1)

修改之后的代码如下:

class SecondLinearTransformation(LinearTransformationScene):
    CONFIG = {
        "include_background_plane": True,
        "include_foreground_plane": True,
        "foreground_plane_kwargs": {
            "x_max": 2*FRAME_WIDTH / 2,
            "x_min": -2*FRAME_WIDTH / 2,
            "y_max": 2*FRAME_WIDTH / 2,
            "y_min": -2*FRAME_WIDTH / 2,
            "faded_line_ratio": 0
        },
        "background_plane_kwargs": {
            "color": GREY,
            "axis_config": {
                "stroke_color": LIGHT_GREY,
            },
            "axis_config": {
                "color": GREY,
            },
            "background_line_style": {
                "stroke_color": GREY,
                "stroke_width": 1,
            },
        },
        "show_coordinates": True,
        "show_basis_vectors": True,
        "basis_vector_stroke_width": 6,
        "i_hat_color": GREEN,
        "j_hat_color": RED,
        "leave_ghost_vectors": False,
    }
    def construct(self):
        mob = Circle(color=PURPLE)
        mob.move_to(RIGHT+UP*2)
        vector_array1 = np.array([[1], [2]])
        vector_array2 = np.array([[2], [1]])
        matrix = [[2, 0], [1, 1]]
        title1 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}2&0\\1&1\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}1\\0\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}2\\1\end{array}} \right]").set_color(GREEN)
        title2 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}2&0\\1&1\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]").set_color(RED)
        title3 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}2&0\\1&1\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}1\\2\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}2\\3\end{array}} \right]").set_color(YELLOW)
        title4 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}2&0\\1&1\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}2\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}4\\3\end{array}} \right]").set_color(BLUE)
        TextGroup=VGroup(title1,title2,title3,title4).arrange_submobjects(DOWN,aligned_edge=LEFT,buff=0.5).shift(4*LEFT)
        
        self.add_transformable_mobject(mob)
        self.add_vector(vector_array1,color=YELLOW)
        self.add_vector(vector_array2,color=BLUE)
        self.apply_matrix(matrix)
        self.add_title(TextGroup,animate=True)
        self.wait(3)

输出结果为:

关于 CONFIG{} 字典的用途,在 3Blue1Brown 动画制作教程(5) 中已经介绍过了;关于公式的编辑,在 3Blue1Brown 动画制作教程(4) 也已经讲解过了。

也就是说,如果前面几篇都认真学过,读懂上面的代码应该是不成问题的。依据上面的动画,可以直观看到二维线性变换的本质。

参照这个例子,并结合 LinearTransformationScene 类中定义的函数,就可以去构建自己想要的任意一种二维平面的线性变换动画了,授人以鱼不如授人以渔。

给一个纯旋转的例子:

class ThirdLinearTransformation(LinearTransformationScene):
    CONFIG = {
        "include_background_plane": True,
        "include_foreground_plane": True,
        "foreground_plane_kwargs": {
            "x_max": 2*FRAME_WIDTH / 2,
            "x_min": -2*FRAME_WIDTH / 2,
            "y_max": 2*FRAME_WIDTH / 2,
            "y_min": -2*FRAME_WIDTH / 2,
            "faded_line_ratio": 0
        },
        "background_plane_kwargs": {
            "color": GREY,
            "axis_config": {
                "stroke_color": LIGHT_GREY,
            },
            "axis_config": {
                "color": GREY,
            },
            "background_line_style": {
                "stroke_color": GREY,
                "stroke_width": 1,
            },
        },
        "show_coordinates": True,
        "show_basis_vectors": True,
        "basis_vector_stroke_width": 6,
        "i_hat_color": GREEN,
        "j_hat_color": RED,
        "leave_ghost_vectors": False,
    }
    def construct(self):
        mob = Circle(color=PURPLE)
        mob.move_to(RIGHT+UP*2)
        vector_array1 = np.array([[1], [2]])
        vector_array2 = np.array([[2], [1]])
        matrix = [[0, -1], [1, 0]]
        title11 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&-1\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}1\\0\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]").set_color(GREEN)
        title21 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&-1\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}-1\\0\end{array}} \right]").set_color(RED)
        title31 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&-1\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}1\\2\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}-2\\1\end{array}} \right]").set_color(YELLOW)
        title41 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&-1\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}2\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}-1\\2\end{array}} \right]").set_color(BLUE)
        TextGroup1=VGroup(title11,title21,title31,title41).arrange_submobjects(DOWN,aligned_edge=LEFT,buff=0.5).shift(3.5*RIGHT)
        title12 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&1\\-1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}1\\0\end{array}} \right]").set_color(GREEN)
        title22 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&1\\-1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}-1\\0\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]").set_color(RED)
        title32 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&1\\-1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}-2\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}1\\2\end{array}} \right]").set_color(YELLOW)
        title42 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}0&1\\-1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}-1\\2\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}2\\1\end{array}} \right]").set_color(BLUE)
        TextGroup2=VGroup(title12,title22,title32,title42).arrange_submobjects(DOWN,aligned_edge=LEFT,buff=0.5).shift(3.5*LEFT)
                            
        self.add_transformable_mobject(mob)
        self.add_vector(vector_array1,color=YELLOW)
        self.add_vector(vector_array2,color=BLUE)
        self.apply_matrix(matrix)
        self.add_title(TextGroup1,animate=True)
        self.wait(3)
        self.play(FadeOut(TextGroup1))
        self.apply_inverse(matrix) 
        self.add_title(TextGroup2,animate=True)
        self.wait(3)

输出结果为:

这里的变换矩阵是 \left[ {\begin{array}{*{20}{c}}0&-1\\1&0\end{array}} \right] ,是一个典型的旋转矩阵,对应的其实就是 \left[ {\begin{array}{*{20}{c}}\cos(90^\circ)&-\sin(90^\circ)\\\sin(90^\circ)&\cos(90^\circ)\end{array}} \right] ,可以理解为:旋转角度为逆时针 90^\circ 的旋转矩阵。

代码最后的 apply_inverse() 方法通过输入一个矩阵得到它的逆矩阵,并按照这个逆矩阵进行线性变换。

众所周知,变换矩阵 \left[ {\begin{array}{*{20}{c}}0&-1\\1&0\end{array}} \right] 的逆矩阵是 \left[ {\begin{array}{*{20}{c}}0&1\\-1&0\end{array}} \right] ,也是一个典型的旋转矩阵,对应的其实就是 \left[ {\begin{array}{*{20}{c}}\cos(-90^\circ)&-\sin(-90^\circ)\\\sin(-90^\circ)&\cos(-90^\circ)\end{array}} \right],可以理解为:旋转角度为逆时针旋转角度为顺时针 90^\circ 的旋转矩阵。

所以整个动画表现出来的结果就是先逆时针旋转 90^\circ ,然后再顺时针旋转 90^\circ 回到初始状态。


最后再给一个奇异矩阵的线性变换,看看其效果如何:

class FourthLinearTransformation(LinearTransformationScene):
    CONFIG = {
        "include_background_plane": True,
        "include_foreground_plane": True,
        "foreground_plane_kwargs": {
            "x_max": 2*FRAME_WIDTH / 2,
            "x_min": -2*FRAME_WIDTH / 2,
            "y_max": 2*FRAME_WIDTH / 2,
            "y_min": -2*FRAME_WIDTH / 2,
            "faded_line_ratio": 0
        },
        "background_plane_kwargs": {
            "color": GREY,
            "axis_config": {
                "stroke_color": LIGHT_GREY,
            },
            "axis_config": {
                "color": GREY,
            },
            "background_line_style": {
                "stroke_color": GREY,
                "stroke_width": 1,
            },
        },
        "show_coordinates": True,
        "show_basis_vectors": True,
        "basis_vector_stroke_width": 6,
        "i_hat_color": GREEN,
        "j_hat_color": RED,
        "leave_ghost_vectors": False,
    }
    def construct(self):
        mob = Circle(color=PURPLE)
        mob.move_to(RIGHT+UP*2)
        vector_array1 = np.array([[1], [2]])
        vector_array2 = np.array([[2], [1]])
        matrix = [[1, 0], [1, 0]]
        title1 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}1&0\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}1\\0\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}1\\1\end{array}} \right]").set_color(GREEN)
        title2 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}1&0\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}0\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}0\\0\end{array}} \right]").set_color(RED)
        title3 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}1&0\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}1\\2\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}1\\1\end{array}} \right]").set_color(YELLOW)
        title4 = TexMobject(r"\left[ {\begin{array}{*{20}{c}}1&0\\1&0\end{array}} \right]\cdot\
                            \left[ {\begin{array}{*{20}{c}}2\\1\end{array}} \right]=\
                            \left[ {\begin{array}{*{20}{c}}2\\2\end{array}} \right]").set_color(BLUE)
        TextGroup=VGroup(title1,title2,title3,title4).arrange_submobjects(DOWN,aligned_edge=LEFT,buff=0.5).shift(3.5*LEFT)
                            
        self.add_transformable_mobject(mob)
        self.add_vector(vector_array1,color=YELLOW)
        self.add_vector(vector_array2,color=BLUE)
        self.apply_matrix(matrix)
        self.add_title(TextGroup,animate=True)
        self.wait(3)

其输出为:

可以看出依据奇异线性变换矩阵 \left[ {\begin{array}{*{20}{c}}1&0\\1&0\end{array}} \right] 进行线性变换后,整个平面被压缩到了一条直线上。


小结

制作线性变换部分的动画是我一直以来的一个梦想,如今终于有了些眉目,当然还有很多酷炫的函数我还没有尝试过,还有很多需要学习的东西。相信你也可以通过不断的练习,做出让自己和别人都满意的教学动画。


更多内容,尽在专栏:

#原创文章,知乎首发,

未经允许,不得转载#

编辑于 2020-03-05 16:11