updater 文档 | 第 1 章 前置知识
从 Mobject 到 Animation 再到 Scene,分析 manim 是如何构建动画的
注意
该文档为 widcardw 个人理解,如有任何描述不得体,或分析错误的地方,欢迎读者指出。
另外,欢迎读者到 B 站观看笔者在很早以前做的讲解视频,或许对大家的理解有帮助
Mobject
manim 中,能够呈现在屏幕上的所有物件,包括基本图形、文字、坐标系等等,这些都是继承于 Mobject 类的。
不管是 cairo 版本还是 gl 版本,它们都会在创建 Mobject 实例的时候进行一系列的初始化,在这初始化的过程中,其实就有一小部分涉及到了图形接口。虽然两种版本的接口有所不同,但大体上都是初始化锚点、颜色等等(gl 版本增加了一些事件侦听、 shader data 之类的元素)。在做好物件 自身数据 相关的初始化之后,就需要将这一“管道”连接到图形接口上,在这些流程走完后,才能将我们所想要的画面呈现在屏幕上。
wid 曾无数次劝新手通过看源码来学习 manim ,因为写教程的人真的太少了 ¯\_(ツ)_/¯
在有编程基础的情况下,反而看源码成了效率最高的学习方式。
然而,我们看源码也是挺头疼的,有时还会忽略一些要点。
没错,就是这个 init_updaters
方法。它的具体代码是这样的:
这一个初始化操作,就是为 Mobject 实例的成员中,注入了两个 updater 的列表,以及一些“开关”来控制物体是否进行更新。有了这些成员变量,才能驱使 Mobject 能够在这个动画引擎中发生更新。
我们可以看到,每次更新都会触发 Mobject 调用列表中的方法,从而更新 Mobject 自身的状态,这样就会让视频的每一帧都出现不同的画面,也就是我们所看到的动画了。
Animation
动画是 manim 中一项极为惊艳的特征,因为这些动画看上去相当优雅,很有观赏性。正因为它的效果震住了我们, 于是我们也就理所应当地认为动画的实现很复杂。
然而事实上不是这样的,它的代码实现其实反而出乎意料的简单。
一些动画的概念
首先,动画其实也是有 finite 和 infinite 的区分,也就是说,动画可以分为有始有终的动画和无限播放的动画。
- 针对有穷动画,大多数情况下,我们都会要求它有起始点和结束点,分别定义为
begin
和 finish
- 针对无穷动画,我们可能只需要考虑它的起始点,之后就可以任其发展了
在这里所提到的,继承于 Animation
的动画,大多是有穷动画。而有穷动画的过程,大多都是由插值来完成的。
我们常常这样编写动画
书写动画这么照着分析,可以分成两个过程
Write(text)
看上去是一个函数调用,其实应当是创建了一个 Write
的实例
- Scene 通过
play
方法执行这个动画的实例,生成了书写的动画
而在第一步过程中,其实已经做了不少处理。如果我们去阅读 Animation
的源码,发现它定义了很多的方法,但是并没有直接在类当中连接起来,相互调用,形成动画。这是因为所有的动画,其实都应当依托于 Scene
这个“大容器”,只有在场景中,动画才能播放的起来。
我们先来看看 Animation
都定义了哪些方法
当然,Animation
的方法肯定不止这些,此处只是列出了一些在逻辑上比较关键的
- 初始化过程中,首先是一些参数的传递,以及将需要播放动画的物件放到自己的成员变量中
begin
方法会预估动画的起止时间,创建初始物件的拷贝,以及开启互斥锁,将动画设置到开始状态
finish
方法会做好动画结束的后处理,包括将动画设置到结束的状态,关闭互斥锁等等(其实还有将动画中无用物件清理的方法 clean_up_from_scene
,也可以认为这是后处理)
interpolate
插值就是动画中最为核心的概念,它的作用是生成起止点中间过渡的画面
源码
之所以要创建初始物件的拷贝,其实也是为了能够在插值的过程中,可以方便调用。在动画运行的过程中,物件自身会随着动画不断的变化,而 starting_submobject
永远不变,为生成中间物件做准备。
对于像是 Write
之类的看上去比较高级的动画效果,不过是在实现上加了一点点细节罢了,其本质依然是生成补间动画。
^how-write-works
Write 原理 (较长 酌情阅读)
这个动画其实分为两个过程:绘制轮廓、淡入填充。下面对其实现细节进行分析
在 begin
方法中,首先需要拷贝一份物件的轮廓,而后才能开启动画。其中 lock_data
的作用是对动画的执行效率进行了一些优化
此处实现了 Animation
的抽象方法 interpolate_submobject
。integer_interpolate
函数将 alpha
从 [0,1] 的定义域拆成两部分
- 当 index = 0 时,仅仅绘制轮廓
- 当到达两段动画的交界点时,做一些数据处理和效率优化
- 当 index = 1 时,仅仅绘制填充色
这样 Write
动画就完成了。
有人说:“不对呀,为什么 Animation
的 interpolate_submobject
抽象方法只接收 3 个参数,而这里能接受 4 个参数呢?”
这是“黑魔法”!
还记得是谁调用 interpolate_submobject
的吗?是 interpolate_mobject
这里的 *mob
提取了动画的成员变量 families
,也就是说,参数数量其实就取决于这里的成员变量。而 Write
又重写了 get_all_mobjects
方法,让 families
发生了变化
如果用 PyCharm 打开 manim 工程,会发现 Write
的这个方法有警告,确实是黑魔法。
Scene
上面也提到了,Animation
所有的方法其实都是要由 Scene
来调用的,play
方法的描述大致如下
begin_animations
方法调用了所有传入 play
的动画的 begin
方法,即开启了所有的动画。
progress_through_animations
方法会在每一帧都生成一幅画面,这些画面串接起来称为了动画。
finish_animations
方法则会结束所有传入的动画,做之后的处理。
除此之外,Scene 还实现了一些与 updater 有关的方法:
这个方法的作用就是遍历场景中的所有物件,并根据他们自身 updater list 成员变量进行更新。这个方法的调用者其实也有不少,包括 play
方法和 wait
方法等。
有了这些基础,就可以将这一份份建材磊起来,创造 manim 的动画了。
其他的部分,与本文档要叙述的部分关联并非十分紧密,暂时只是一笔带过。
#manim
#updater
#教程