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] 进行线性变换后,整个平面被压缩到了一条直线上。
小结
制作线性变换部分的动画是我一直以来的一个梦想,如今终于有了些眉目,当然还有很多酷炫的函数我还没有尝试过,还有很多需要学习的东西。相信你也可以通过不断的练习,做出让自己和别人都满意的教学动画。
更多内容,尽在专栏: