文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。
Manim 的构建模块
本文档解释了 Manim 的构建模块,并将为你提供制作自己的视频所需的所有工具。
本质上,Manim 为你提供了三种可以协同使用来制作数学动画的概念:数学对象(或简称 mobject)、动画 和 场景。正如我们将在以下各节中看到的,这三个概念中的每一个都在 Manim 中作为一个单独的类实现,即 Mobject
、Animation
和 Scene
类。
注意:建议您在阅读本页之前先阅读教程快速入门和 Manim 的输出设置。
Mobjects
Mobjects 是所有 Manim 动画的基本构建模块。从 Mobject
派生的每个类都代表一个可以在屏幕上显示的对象。例如,简单的形状,如 Circle
(圆形)、Arrow
(箭头)和 Rectangle
(矩形)都是 mobjects。更复杂的构造,如 Axes
(坐标轴)、FunctionGraph
(函数图像)或 BarChart
(柱状图)也是 mobjects。
如果你试图在屏幕上显示一个 Mobject
的实例,你只会看到一个空白的框架。原因是 Mobject
类是所有其他 mobjects 的抽象基类,即它没有可以在屏幕上显示的预定义视觉形状。它只是可以显示的某种事物的骨架。因此,你很少需要使用普通的 Mobject
实例;相反,你很可能会创建其派生类的实例。其中的一个派生类是 VMobject
。V
代表 Vectorized Mobject(矢量化对象)。本质上,vmobject 是一个使用矢量图形显示的 mobject。大多数情况下,你将处理 vmobjects,尽管我们将继续使用“mobject”一词来指代可以在屏幕上显示的形状类别,因为它更通用。
注意:任何可以在屏幕上显示的对象都是一个 mobject
,即使它不一定是“数学的”。
提示:要查看从 Mobject
派生的类的示例,请参阅 geometry
模块。其中大多数实际上也是从 VMobject
派生的。
创建和显示 mobjects
如快速入门中所述,Manim 脚本中的所有代码通常都放在 Scene
类的 construct()
方法中。要将 mobject 显示在屏幕上,请调用包含该 mobject 的 Scene
的 add()
方法。这是在没有动画的情况下将 mobject 显示在屏幕上的主要方式。要从屏幕上移除 mobject,只需从包含它的 Scene
中调用 remove()
方法。
示例:CreatingMobjects
from manim import *
class CreatingMobjects(Scene):
def construct(self):
circle = Circle()
self.add(circle)
self.wait(1)
self.remove(circle)
self.wait(1)
定位 mobjects
让我们定义一个名为 Shapes
的新 Scene
,并添加一些 mobjects。此脚本生成一个静态图片,显示一个圆形、一个正方形和一个三角形:
示例:Shapes
from manim import *
class Shapes(Scene):
def construct(self):
circle = Circle()
square = Square()
triangle = Triangle()
circle.shift(LEFT)
square.shift(UP)
triangle.shift(RIGHT)
self.add(circle, square, triangle)
self.wait(1)
默认情况下,创建 mobjects 时,它们会被放置在坐标中心,或称 原点。它们还会被赋予一些默认颜色。此外,Shapes
场景使用 shift()
方法来定位 mobjects。正方形从原点向上移动一个单位,而圆形和三角形则分别向左和向右移动一个单位。
注意:与其他图形软件不同,Manim 将坐标中心置于屏幕中央。正向垂直方向是向上,正向水平方向是向右。请参阅 constants
模块中定义的常量 ORIGIN
、UP
、DOWN
、LEFT
、RIGHT
等。
还有许多其他可能的方法可以在屏幕上放置 mobjects,例如 move_to()
、next_to()
和 align_to()
。下一个场景 MobjectPlacement
使用了全部三种方法。
示例:MobjectPlacement
from manim import *
class MobjectPlacement(Scene):
def construct(self):
circle = Circle()
square = Square()
triangle = Triangle()
# 将圆形放置在原点左侧两个单位的位置
circle.move_to(LEFT * 2)
# 将正方形放置在圆形的左侧
square.next_to(circle, LEFT)
# 将三角形的左侧边界与圆形的左侧边界对齐
triangle.align_to(circle, LEFT)
self.add(circle, square, triangle)
self.wait(1)
move_to()
方法使用绝对单位(相对于 ORIGIN
测量),而 next_to()
使用相对单位(从作为第一个参数传递的 mobject 测量)。align_to()
使用 LEFT
不是作为测量单位,而是作为确定用于对齐的边框的方式。mobject 的边框坐标是通过其周围的虚拟边界框来确定的。
提示:Manim 中的许多方法可以链式调用。例如,以下两行代码:
square = Square()
square.shift(LEFT)
可以替换为:
square = Square().shift(LEFT)
从技术上讲,这是可能的,因为大多数方法调用会返回修改后的 mobject。
设置 mobjects 的样式
以下场景改变了 mobjects 的默认视觉效果。
示例:MobjectStyling
from manim import *
class MobjectStyling(Scene):
def construct(self):
circle = Circle().shift(LEFT)
square = Square().shift(UP)
triangle = Triangle().shift(RIGHT)
circle.set_stroke(color=GREEN, width=20)
square.set_fill(YELLOW, opacity=1.0)
triangle.set_fill(PINK, opacity=0.5)
self.add(circle, square, triangle)
self.wait(1)
此场景使用了两个主要的函数来改变 mobject 的视觉样式:set_stroke()
和 set_fill()
。前者改变 mobject 边框的视觉样式,而后者改变内部的样式。默认情况下,大多数 mobjects 的内部是完全透明的,因此你必须指定 opacity
参数才能显示颜色。opacity
的值为 1.0
表示完全不透明,而 0.0
表示完全透明。
只有 VMobject
的实例实现了 set_stroke()
和 set_fill()
。Mobject
的实例则实现了 set_color()
。大多数预定义的类都是从 VMobject
派生的,因此通常可以假设你有权限使用 set_stroke()
和 set_fill()
。
Mobject 的屏幕显示顺序
下一个场景与上一节中的 MobjectStyling
场景完全相同,只有一个地方不同。
示例:MobjectZOrder
from manim import *
class MobjectZOrder(Scene):
def construct(self):
circle = Circle().shift(LEFT)
square = Square().shift(UP)
triangle = Triangle().shift(RIGHT)
circle.set_stroke(color=GREEN, width=20)
square.set_fill(YELLOW, opacity=1.0)
triangle.set_fill(PINK, opacity=0.5)
self.add(triangle, square, circle)
self.wait(1)
这里唯一的不同(除了场景名称)是将 mobjects 添加到场景中的顺序。在 MobjectStyling
中,我们按 add(circle, square, triangle)
的顺序添加它们,而在 MobjectZOrder
中,我们按 add(triangle, square, circle)
的顺序添加它们。
正如你所看到的,add()
参数的顺序决定了 mobjects 在屏幕上的显示顺序,最左边的参数被放置在最底层。
动画
Manim 的核心是动画。通常,你可以通过调用 play()
方法将动画添加到场景中。
示例:SomeAnimations
from manim import *
class SomeAnimations(Scene):
def construct(self):
square = Square()
# 一些动画用于显示 mobjects,...
self.play(FadeIn(square))
# ...一些动画用于移动或旋转 mobjects...
self.play(Rotate(square, PI/4))
# 一些动画用于从屏幕上移除 mobjects
self.play(FadeOut(square))
self.wait(1)
简而言之,动画是两个 mobjects 之间的插值过程。例如,FadeIn(square)
从完全透明的 square
版本开始,以完全不透明的版本结束,通过逐渐增加透明度来插值。FadeOut
则相反:它从完全不透明插值到完全透明。再举一个例子,Rotate
从传递给它的 mobject 开始,以相同对象但旋转了一定角度的版本结束,这次是通过插值 mobject 的角度而不是透明度。
动画方法
可以改变的任何 mobject 属性都可以被动画化。实际上,任何改变 mobject 属性的方法都可以通过使用 animate()
来作为动画。
示例:AnimateExample
from manim import *
class AnimateExample(Scene):
def construct(self):
square = Square().set_fill(RED, opacity=1.0)
self.add(square)
# 动画化颜色变化
self.play(square.animate.set_fill(WHITE))
self.wait(1)
# 同时动画化位置变化和旋转
self.play(square.animate.shift(UP).rotate(PI / 3))
self.wait(1)
引用:Animation
animate()
是所有 mobjects 的一个属性,它将后续的方法动画化。例如,square.set_fill(WHITE)
设置正方形的填充颜色,而 square.animate.set_fill(WHITE)
则将此操作动画化。
动画运行时间
默认情况下,传递给 play()
的任何动画持续时间正好为一秒钟。使用 run_time
参数来控制持续时间。
示例:RunTime
from manim import *
class RunTime(Scene):
def construct(self):
square = Square()
self.add(square)
self.play(square.animate.shift(UP), run_time=3)
self.wait(1)
创建自定义动画
尽管 Manim 有许多内置动画,但你可能会遇到需要从一个 Mobject
的状态平滑过渡到另一个状态的情况。如果你发现自己处于这种情况,那么你可以定义自己的自定义动画。你可以通过扩展 Animation
类并覆盖其 interpolate_mobject()
方法来开始。interpolate_mobject()
方法接收一个从 0 开始并在动画过程中变化的 alpha 参数。因此,你只需要根据其 interpolate_mobject
方法中的 alpha 值在动画中操作 self.mobject
。然后,你就可以获得 Animation
的所有好处,例如以不同的运行时间播放它或使用不同的速率函数。
假设你从一个数字开始,想要创建一个 Transform
动画将其转换为目标数字。你可以使用 FadeTransform
,它会淡出起始数字并淡入目标数字。但当我们考虑从一个数字转换到另一个数字时,一种直观的方法是通过递增或递减来平滑地进行转换。Manim 提供了一个功能,允许你通过定义自己的自定义动画来自定义这种行为。
你可以从扩展 Animation
的 Count
类开始。该类可以有三个参数的构造函数,即 DecimalNumber
Mobject、起始值和结束值。构造函数将 DecimalNumber
Mobject 传递给超类构造函数(在这种情况下,是 Animation
构造函数),并设置起始值和结束值。
你唯一需要做的是定义在动画的每一步你希望它看起来像什么。Manim 会在 interpolate_mobject()
方法中根据视频的帧率、速率函数和动画的播放时间为你提供 alpha 值。alpha 参数持有介于 0 和 1 之间的值,表示当前播放动画的步骤。例如,0 表示动画的开始,0.5 表示动画进行到一半,1 表示动画的结束。
在 Count
动画的情况下,你只需要想出一种方法来确定在给定 alpha 值时要显示的数字,然后在 Count
动画的 interpolate_mobject()
方法中设置该值。假设你从 50 开始,并在动画结束时将 DecimalNumber
增加到 100。
-
如果 alpha 是 0,你希望值是 50。
-
如果 alpha 是 0.5,你希望值是 75。
-
如果 alpha 是 1,你希望值是 100。
通常,你从起始数字开始,并根据 alpha 值只增加一部分要增加的值。因此,计算每步要显示的数字的逻辑将是 50 + alpha * (100 - 50)
。一旦你为 DecimalNumber
设置了计算出的值,你就完成了。
一旦你定义了自己的 Count
动画,你就可以在场景中播放它,持续时间由你决定,适用于任何 DecimalNumber
,并使用任何速率函数。
示例:CountingScene
from manim import *
class Count(Animation):
def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
# 将 number 作为动画的 mobject 传递
super().__init__(number, **kwargs)
# 设置起始值和结束值
self.start = start
self.end = end
def interpolate_mobject(self, alpha: float) -> None:
# 根据 alpha 设置 DecimalNumber 的值
value = self.start + (alpha * (self.end - self.start))
self.mobject.set_value(value)
class CountingScene(Scene):
def construct(self):
# 创建 DecimalNumber 并将其添加到场景中
number = DecimalNumber().set_color(WHITE).scale(5)
# 添加一个更新器,以在值变化时保持 DecimalNumber 居中
number.add_updater(lambda number: number.move_to(ORIGIN))
self.add(number)
self.wait()
# 播放 Count 动画,在 4 秒内从 0 计数到 100
self.play(Count(number, 0, 100), run_time=4, rate_func=linear)
self.wait()
引用:Animation
DecimalNumber
interpolate_mobject()
play()
使用 mobject 的坐标
Mobjects 包含定义其边界的点。这些点可以用来根据彼此的位置添加其他 mobjects,例如通过 get_center()
、get_top()
和 get_start()
等方法。以下是一些重要坐标的示例:
示例:MobjectExample
from manim import *
class MobjectExample(Scene):
def construct(self):
p1 = [-1,-1, 0]
p2 = [ 1,-1, 0]
p3 = [ 1, 1, 0]
p4 = [-1, 1, 0]
a = Line(p1,p2).append_points(Line(p2,p3).points).append_points(Line(p3,p4).points)
point_start = a.get_start()
point_end = a.get_end()
point_center = a.get_center()
self.add(Text(f"a.get_start() = {np.round(point_start,2).tolist()}", font_size=24).to_edge(UR).set_color(YELLOW))
self.add(Text(f"a.get_end() = {np.round(point_end,2).tolist()}", font_size=24).next_to(self.mobjects[-1],DOWN).set_color(RED))
self.add(Text(f"a.get_center() = {np.round(point_center,2).tolist()}", font_size=24).next_to(self.mobjects[-1],DOWN).set_color(BLUE))
self.add(Dot(a.get_start()).set_color(YELLOW).scale(2))
self.add(Dot(a.get_end()).set_color(RED).scale(2))
self.add(Dot(a.get_top()).set_color(GREEN_A).scale(2))
self.add(Dot(a.get_bottom()).set_color(GREEN_D).scale(2))
self.add(Dot(a.get_center()).set_color(BLUE).scale(2))
self.add(Dot(a.point_from_proportion(0.5)).set_color(ORANGE).scale(2))
self.add(*[Dot(x) for x in a.points])
self.add(a)
将 mobjects 转换为其他 mobjects
也可以将一个 mobject 转换为另一个 mobject,如下所示:
示例:ExampleTransform
from manim import *
class ExampleTransform(Scene):
def construct(self):
self.camera.background_color = WHITE
m1 = Square().set_color(RED)
m2 = Rectangle().set_color(RED).rotate(0.2)
self.play(Transform(m1,m2))
Transform
函数将前一个 mobject 的点映射到下一个 mobject 的点。这可能会导致奇怪的行为,例如当一个 mobject 的点按顺时针方向排列而另一个点按逆时针方向排列时。在这种情况下,使用翻转函数并通过 numpy 的 roll
函数重新定位点可能会有所帮助:
示例:ExampleRotation
from manim import *
class ExampleRotation(Scene):
def construct(self):
self.camera.background_color = WHITE
m1a = Square().set_color(RED).shift(LEFT)
m1b = Circle().set_color(RED).shift(LEFT)
m2a = Square().set_color(BLUE).shift(RIGHT)
m2b = Circle().set_color(BLUE).shift(RIGHT)
points = m2a.points
points = np.roll(points, int(len(points)/4), axis=0)
m2a.points = points
self.play(Transform(m1a,m1b),Transform(m2a,m2b), run_time=1)
场景
Scene
类是 Manim 的连接组织。每个 mobject 都必须被 add
到场景中才能显示,或者被 remove
以停止显示。每个动画都必须由场景 play
,并且没有动画发生的时间间隔由对 wait()
的调用决定。你的视频的所有代码都必须包含在从 Scene
派生的类的 construct()
方法中。最后,如果要同时渲染多个场景,一个文件可以包含多个 Scene
子类。
风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。