数学视频动画引擎Python库 -- Manim 的构建模块 building blocks

文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。

Mathematical Animation Engine

Manim 的构建模块

本文档解释了 Manim 的构建模块,并将为你提供制作自己的视频所需的所有工具。

本质上,Manim 为你提供了三种可以协同使用来制作数学动画的概念:数学对象(或简称 mobject)、动画场景。正如我们将在以下各节中看到的,这三个概念中的每一个都在 Manim 中作为一个单独的类实现,即 MobjectAnimationScene 类。

注意:建议您在阅读本页之前先阅读教程快速入门Manim 的输出设置

Mobjects

Mobjects 是所有 Manim 动画的基本构建模块。从 Mobject 派生的每个类都代表一个可以在屏幕上显示的对象。例如,简单的形状,如 Circle(圆形)、Arrow(箭头)和 Rectangle(矩形)都是 mobjects。更复杂的构造,如 Axes(坐标轴)、FunctionGraph(函数图像)或 BarChart(柱状图)也是 mobjects。

如果你试图在屏幕上显示一个 Mobject 的实例,你只会看到一个空白的框架。原因是 Mobject 类是所有其他 mobjects 的抽象基类,即它没有可以在屏幕上显示的预定义视觉形状。它只是可以显示的某种事物的骨架。因此,你很少需要使用普通的 Mobject 实例;相反,你很可能会创建其派生类的实例。其中的一个派生类是 VMobjectV 代表 Vectorized Mobject(矢量化对象)。本质上,vmobject 是一个使用矢量图形显示的 mobject。大多数情况下,你将处理 vmobjects,尽管我们将继续使用“mobject”一词来指代可以在屏幕上显示的形状类别,因为它更通用。

注意:任何可以在屏幕上显示的对象都是一个 mobject,即使它不一定是“数学的”。

提示:要查看从 Mobject 派生的类的示例,请参阅 geometry 模块。其中大多数实际上也是从 VMobject 派生的。

创建和显示 mobjects

快速入门中所述,Manim 脚本中的所有代码通常都放在 Scene 类的 construct() 方法中。要将 mobject 显示在屏幕上,请调用包含该 mobject 的 Sceneadd() 方法。这是在没有动画的情况下将 mobject 显示在屏幕上的主要方式。要从屏幕上移除 mobject,只需从包含它的 Scene 中调用 remove() 方法。

CreatingMobjects

示例: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

示例: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 模块中定义的常量 ORIGINUPDOWNLEFTRIGHT 等。

还有许多其他可能的方法可以在屏幕上放置 mobjects,例如 move_to()next_to()align_to()。下一个场景 MobjectPlacement 使用了全部三种方法。

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

示例: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

示例: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

示例: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

示例: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

示例: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 提供了一个功能,允许你通过定义自己的自定义动画来自定义这种行为。

你可以从扩展 AnimationCount 类开始。该类可以有三个参数的构造函数,即 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

示例: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

示例: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

示例: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

示例: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 子类。

风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

船长Q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值