dustland

dustball in dustland

manim helloworld

Manim 入门

参考快速入门 - ManimGL 文档

在manim根目录下面建立start.py文件

1
2
3
4
5
6
7
8
manim/
├── manimlib/
│ ├── animation/
│ ├── ...
│ ├── default_config.yml
│ └── window.py
├── (custom_config.yml)
└── start.py

从例程开始

1
2
3
4
5
6
7
8
9
from manimlib import *

class SquareToCircle(Scene):
def construct(self):
circle = Circle()
circle.set_fill(RED, opacity=1)
circle.set_stroke(PINK, width=4)

self.add(circle)

执行命令

1
manimgl start.py SquareToCircle

产生一个红色粉圈的圆点

image-20220906154543534

可以使用键盘鼠标移动画面

  • 滚动鼠标中键来上下移动画面
  • 按住键盘上 z 键的同时滚动鼠标中键来缩放画面
  • 按住键盘上 f 键的同时移动鼠标来平移画面
  • 按住键盘上 d 键的同时移动鼠标来改变三维视角
  • 按下键盘上 r 键恢复到最初的视角

最后,你可以通过按 q 来关闭窗口并退出程序.

整个代码算是定义了一个类SquareToCircle,它是Scene的子类,然后重写了父类的函数construct

1
2
3
4
def construct(self) -> None:
# Where all the animation happens
# To be implemented in subclasses
pass

从这个函数中的注释看,应该在该函数中定义所有动画.

然而貌似这只是创建了一个类,并没有实例化啊

在执行命令时已经暗藏玄机了

为了运行这个程序绘制动画,我们并没有使用

1
python start.py

这个命令,如果真的这样写了什么都不会发生,因为我们就创建了一个子类但是没有实例化,相当于没有任何动作

我们事实上用到的命令是

1
manimgl start.py SquareToCircle

这后面跟着一个参数SquareToCircle,可以大胆的猜测,

manimgl使用start.py这个文件中的SquareToCircle类创造实例去了,然后就执行了construct函数中的动作

之前从来没有学过python,从manim的main模块入手,看看控制流是怎么样的

从命令行开始的控制流

main模块

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
#!/usr/bin/env python
from manimlib import __version__
import manimlib.config
import manimlib.extract_scene
import manimlib.logger
import manimlib.utils.init_config


def main():
print(f"ManimGL \033[32mv{__version__}\033[0m")

args = manimlib.config.parse_cli()#命令行语法分析
if args.version and args.file is None:#
return
if args.log_level:
manimlib.logger.log.setLevel(args.log_level)

if args.config:
manimlib.utils.init_config.init_customization()
else:
config = manimlib.config.get_configuration(args)
scenes = manimlib.extract_scene.main(config)

for scene in scenes:
scene.run()


if __name__ == "__main__":
main()

main函数里首先打印了manim版本信息,运行时也是这样的

1
2
3
PS D:\manim> manimgl start.py SquareToCircle 
ManimGL v1.6.1
...

然后调用了manim命令行的语法分析器,将结果保存到args数组中

这个语法分析器干了啥呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def parse_cli():
try:
parser = argparse.ArgumentParser()
module_location = parser.add_mutually_exclusive_group()
module_location.add_argument(
"file",
nargs="?",
help="Path to file holding the python code for the scene",
)
parser.add_argument(
"scene_names",
nargs="*",
help="Name of the Scene class you want to see",
)
parser.add_argument...
parser.add_argument(
"--log-level",
help="Level of messages to Display, can be DEBUG / INFO / WARNING / ERROR / CRITICAL"
)
args = parser.parse_args()
return args

调用了python自带的参数语法分析器,然后给他添加了很多规则

目标模块位置不包括在参数内,目标模块位置的键叫做"file"

对于参数,添加了一些键比如"scene_names","--log-level"等等

最后用args数组保存语法分析器的运行结果

以后再写shell的时候可以借鉴python的语法分析器了

因此main函数中调用args.version,args.log_level等等都是args数组中的键了

如果命令行参数有表明本次执行命令是为了修改设置的(if args.config),

则调用manimlib.utils.init_config.init_customization将命令行给出的各种参数保存到库中

否则,即本次命令不是为了设置,那么就是为了运行动画了

调用manimlib.extract_scene.main用scenes数组保存所有场景类的实例,

这个函数干了啥呢?

1
2
3
4
5
6
7
8
9
10
def main(config):
module = config["module"]
scene_config = get_scene_config(config)
if module is None:
# If no module was passed in, just play the blank scene
return [BlankScene(**scene_config)]

all_scene_classes = get_scene_classes_from_module(module)
scenes = get_scenes_to_render(all_scene_classes, scene_config, config)
return scenes

config来自库中的设置,也可以命令行上修改

module获取命令行指定的模块名

scene_config获取场景设置

如果命令行上没有传递目标模块,则执行默认的黑色场景

all_scene_classes从目标模块module中获取所有的scene子类

1
2
3
4
5
6
7
8
9
10
11
def get_scene_classes_from_module(module):
if hasattr(module, "SCENES_IN_ORDER"):
return module.SCENES_IN_ORDER
else:
return [
member[1]
for member in inspect.getmembers(
module,
lambda x: is_child_scene(x, module)#判断是否是scene的子类
)
]

然后get_scenes_to_render将每个scene子类实例化,并返回对象数组

然后遍历scenes数组,逐一运行每一个场景实例

1
2
for scene in scenes:
scene.run()

调用的是每个对象的run函数

run函数干了啥呢?它进行了一些设置,比如设置动画开始运行的时间,

1
2
3
run()->
construct()#执行construct中定义的动画
interact()#允许交互

终于就调用到我们自己写的construct函数了

总结:

主模块干了这么几件事:

1.语法分析,分割命令行

2.获取目标模块中的所有scene的子类,并给每个该种类创建实例

3.遍历执行每个该种类的run函数,run会首先调用construct绘制动画,然后调用interact,允许用户和动画调用

这大概就是控制流的方向

自定义construct

粗略地看完控制流,回到start.py中,看看construct是怎么写的

1
2
3
4
5
6
def construct(self):
circle = Circle()
circle.set_fill(RED, opacity=1)
circle.set_stroke(PINK, width=4)

self.add(circle)

创建了一个Circle类的实例,然后调用两个setter函数设置了图形的填充颜色,边线颜色,

然后把该实例添加到SquareToCircle实例的mobjects数组中,该数组用来存放需要被动画演示的对象

可以大胆推测,当SquareToCircle实例创建之后会遍历mobjects数组演示这些对象

那么Circle类是怎么定义的呢?

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
class Circle(Arc):
CONFIG = {
"color": RED,
"close_new_points": True,
"anchors_span_full_range": False
}

def __init__(self, start_angle: float = 0, **kwargs):
Arc.__init__(self, start_angle, TAU, **kwargs)

def surround(
self,
mobject: Mobject,
dim_to_match: int = 0,
stretch: bool = False,
buff: float = MED_SMALL_BUFF
):
# Ignores dim_to_match and stretch; result will always be a circle
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching

self.replace(mobject, dim_to_match, stretch)
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)

def point_at_angle(self, angle: float) -> np.ndarray:
start_angle = self.get_start_angle()
return self.point_from_proportion(
(angle - start_angle) / TAU
)

def get_radius(self) -> float:
return get_norm(self.get_start() - self.get_center())

这个Circle类已经经过了层层继承,算是曾曾曾...孙子类了

object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
classDiagram 
class object {
python中的万类之源
}
class Mobject{
manim中形状的父类
}
class VMobject{
矢量图形的父类
}
class TipableVMobject{
可以带箭头的矢量图的父类
}
class Arc{
圆弧图形的父类
}
class Circle{
圆圈类
}
object <|-- Mobject
Mobject <|-- VMobject
VMobject <|-- TipableVMobject
TipableVMobject <|-- Arc
Arc <|-- Circle

添加动画

1
2
3
4
5
6
7
8
9
10
11
12
13
from manimlib import *

class SquareToCircle(Scene):
def construct(self):
circle = Circle()
circle.set_fill(BLUE, opacity=0.5)
circle.set_stroke(BLUE_E, width=4)
square = Square()

self.play(ShowCreation(square))
self.wait()
self.play(ReplacementTransform(square, circle))
self.wait()

相比于上一次,增加了一个展示创建矩形和矩形变圆圈的动画

1
2
3
4
5
6
square = Square()

self.play(ShowCreation(square))#展示创建一个矩形的过程
self.wait()#默认停顿DEFAULT_WAIT_TIME 秒
self.play(ReplacementTransform(square, circle))#展示矩形转换为原型的过程
self.wait()

启用交互

在最后加上一句

1
self.embed()

当该函数执行之后,iPython终端会变成

1
2
3
4
5
In [1]: play(circle.animate.stretch(4, dim=0))

In [2]: play(circle.animate.stretch(4, dim=0))

In [3]:

In [n]:之后输入命令就可以了

此时所有的函数都是对作用于selft的,play就相当于self.play,当然写着self.也不会错

在iPython终端上调用play(ReplacementTransform(circle, square)),就实现了从矩形再变灰圆圈

manim命令行

环境变量Path里有一个python<版本号>\Scripts\目录

image-20220906175408295

这下面不知道什么时候就有了

image-20220906175442199

也就是说不从manim 根目录下面也可以使用该命令

1
2
manimgl <code>.py <Scene> <flags>
manim-render <code>.py <Scene> <flags>

其中

<code>.py是源文件

<scene>是要渲染的场景类,如果不写并且<code>.py中只有一个类,则渲染这个类,否则需要列出所有类

<flags>选项

flags选项

常用的有

  • -w 把场景写入文件
  • -o 把场景写入文件并打开
  • -s 跳到最后只展示最后一帧
  • -so 保存最后一帧并打开
  • -n <number> 跳到场景中第 n 个动画
  • -f 打开窗口全屏

-w,-o

比如manimgl start.py -w之后默认会在当前工作目录下面生成一个video文件夹,

里面有一个SquareToCircle.mp4文件,显然是按照场景类名命令的文件名

可以使用--video_dir VIDEO_DIR修改该导出目录

-o选项的作用类似于-w,只不过导出文件后又打开了

-n number

-n <number>跳转到场景中的第n个动画,啥意思呢?

每一个self.play都是一个动画

1
2
self.play(ShowCreation(square)) #第一个动画
self.play(ReplacementTransform(square, circle)) #第二个动画

-n number1,bumber2...只渲染指定的动画

-c <color>

设置背景颜色,比如WHITE就是白色

更多参数

详见命令行参数和配置 - ManimGL 文档

个性化设置

在manim的各级子目录下面都可以建立custom_config.yml文件

子目录终端custom_config.yml会覆盖父目录中custom_config.yml的设置

只要有custom_config.yml,就会覆盖manim/manimlib/default_config.yml中的设置

命令行上的设置又会覆盖所有yml中的设置

1
2
3
4
5
6
7
8
9
10
manim/
├── manimlib/
│ ├── animation/
│ ├── ...
│ ├── default_config.yml
│ └── window.py
├── project/
│ ├── code.py
│ └── custom_config.yml
└── custom_config.yml

也可以在命令行上手工指定配置文件的位置

1
manimgl project/code.py --config_file /path/to/custom_config.yml

这个配置文档应该怎么写呢?

manim/manimlib/default_config.yml给出了样本

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
directories:
mirror_module_path: False #是否在output指定的目录下建立项目并输出文件
output: "" #输出目录需要mirror_module_path: True有效
raster_images: "" #像素图像源
vector_images: "" #矢量图源
sounds: "" #声音文件源
temporary_storage: "" #临时存储占用的目录
tex:
executable: "latex" #编译latex使用的可执行程序
template_file: "tex_template.tex" #使用的latex模板,模板,模板
intermediate_filetype: "dvi" #编译后产生的中间矢量文档类型,latex->dvi;xelatex->xdv
text_to_replace: "[tex_expression]"
universal_import_line: "from manimlib import *" #直接进入交互模式时需要导入的包
style:
font: "Consolas" #默认字体类型
text_alignment: "LEFT" #字体对齐模式
background_color: "#333333" #默认背景颜色,灰色
window_position: UR #窗口在显示器中的位置,默认是Uppder,Right,右上角,U/O/D,L/O/R
window_monitor: 0 #在哪个显示器出现,默认是0号
full_screen: False #是否全屏展示,默认不全屏
break_into_partial_movies: False #是否将每个self.play和self.wait单独分成一个视频
camera_resolutions: #导出分辨率
low: "854x480" #低级分辨率低
med: "1280x720"
high: "1920x1080"
4k: "3840x2160"
default_resolution: "high" #默认是high等级的,已经够用了
fps: 30 #帧率

Manim项目结构

来自Manim 结构 - ManimGL 文档

Manim structure

Manim完整控制流

https://fastly.jsdelivr.net/gh/manim-kindergarten/CDN@master/manimgl_assets/manim_shaders_process_cn.png