首页
/ ManimGL 官方示例场景全解:从交互式调试到三维曲面,读懂 example_scenes.py 的九个官方场景

ManimGL 官方示例场景全解:从交互式调试到三维曲面,读懂 example_scenes.py 的九个官方场景

2026-09-04 23:14:54作者:胡易黎Nicole

本文基于 manim(manimgl,OpenGL 渲染版本)仓库中的官方文档 Example Scenes 与示例文件 example_scenes.py,逐一拆解其中提供的各个场景:交互式开发(self.embed())、.animate 方法动画语法、Text/Tex 文字系统、TransformMatchingTex 等匹配变换、Updater 更新器机制、坐标系与函数图像绘制、三维曲面与相机控制,以及一个综合二维场景。读完本文,你不仅能完整复制运行这些官方场景,还能在源码层面理解每一段示例背后的实现机制,将其应用到自己的数学动画创作中。

一、如何运行这些示例场景

示例文件 example_scenes.py 位于仓库根目录,文件头部注释给出了运行方式与常用命令行参数:

# To watch one of these scenes, run the following:
# manimgl example_scenes.py OpeningManimExample
# Use -s to skip to the end and just save the final frame
# Use -w to write the animation to a file
# Use -o to write it to a file and open it once done
# Use -n <number> to skip ahead to the n'th animation of a scene.

manimgl <场景文件> <场景类名>,配合 -s(跳过动画直接保存最后一帧)、-w(写出视频文件)、-o(写出并自动打开)、-n <n>(跳到场景内第 n 个动画)。场景类都继承 Scene 并实现 construct(self) 方法;二维场景使用默认相机,三维场景则继承 ThreeDScene 或通过 CONFIG 指定 ThreeDCamera

以下按文档 InteractiveDevelopment → AnimatingMethods → TextExample → TexTransformExample → UpdatersExample → CoordinateSystemExample → GraphExample → SurfaceExample → OpeningManimExample 的顺序逐一讲解。

二、InteractiveDevelopment:交互式场景开发

该场景与快速入门(见 Quick Start)中写的场景类似,其价值在于演示 self.embed() 打开交互式终端后如何高效开发场景。完整代码:

from manimlib import *

class InteractiveDevelopment(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()

        # This opens an iPython terminal where you can keep writing
        # lines as if they were part of this construct method.
        # In particular, 'square', 'circle' and 'self' will all be
        # part of the local namespace in that terminal.
        self.embed()

        # Try copying and pasting some of the lines below into
        # the interactive shell
        self.play(ReplacementTransform(square, circle))
        self.wait()
        self.play(circle.animate.stretch(4, 0))
        self.play(Rotate(circle, 90 * DEG))
        self.play(circle.animate.shift(2 * RIGHT).scale(0.25))

        text = Text("""
            In general, using the interactive shell
            is very helpful when developing new scenes
        """)
        self.play(Write(text))

        # In the interactive shell, you can just type
        # play, add, remove, clear, wait, save_state and restore,
        # instead of self.play, self.add, self.remove, etc.

        # To interact with the window, type touch().  You can then
        # scroll in the window, or zoom by holding down 'z' while scrolling,
        # and change camera perspective by holding down 'd' while moving
        # the mouse.  Press 'r' to reset to the standard camera position.
        # Press 'q' to stop interacting with the window and go back to
        # typing new commands into the shell.

        # In principle you can customize a scene to be responsive to
        # mouse and keyboard interactions
        always(circle.move_to, self.mouse_point)

源码视角:embed() 到底做了什么

embed 方法定义在 Scene.embed:仅当场景带有窗口(交互模式)时才生效,它先 stop_skipping() 并强制绘制当前帧,然后创建 InteractiveSceneEmbed(self).launch() 启动内嵌 IPython 终端;退出终端后默认抛出 EndScene 结束场景。

真正的终端逻辑在 InteractiveSceneEmbed 中,几个关键机制都可在源码中验证:

  • 局部命名空间注入get_ipython_shell_for_embedded_scene 通过回溯调用栈拿到 self.embed() 所在 construct 的局部变量,把 squarecircleself 等变量注入终端模块命名空间——这就是文档所说“终端里可以直接使用这些变量”的原因。
  • 快捷键get_shortcuts 向终端注入 playwaitaddremoveclearsave_stateundoredocheckpoint_pastereload 等名字,所以终端里可以直接敲 play(...) 而不是 self.play(...),与文档注释完全一致。
  • 出错视觉反馈ensure_flash_on_error 给终端注册了自定义异常钩子,代码出错时场景窗口边框会闪红(FullScreenRectangle 加红色描边播放 VFadeInThenOut 动画),见 scene_embed.py
  • checkpoint 粘贴checkpoint_paste 会读取剪贴板代码块,若代码块以注释开头,则恢复/记录该注释对应的场景快照,便于反复试验同一段代码。

文档注释中提到的窗口交互快捷键(touch() 后滚动平移、按住 z 缩放、按住 d 改变三维视角、r 复位相机、q 返回终端)对应场景窗口事件循环的交互逻辑,窗口交互提示亦可见 Scene 交互提示 中的日志文案。最后一行 always(circle.move_to, self.mouse_point) 演示了场景响应鼠标的能力:self.mouse_point 是场景内置的鼠标位置追踪对象,配合 always 更新器让圆形始终跟随鼠标。

三、AnimatingMethods:用 .animate 语法动画化任意方法

这个场景引入了两个新用法:.get_grid()self.play(mob.animate.method(args))。完整代码:

class AnimatingMethods(Scene):
    def construct(self):
        grid = OldTex(r"\pi").get_grid(10, 10, height=4)
        self.add(grid)

        # You can animate the application of mobject methods with the
        # ".animate" syntax:
        self.play(grid.animate.shift(LEFT))

        # Both of those will interpolate between the mobject's initial
        # state and whatever happens when you apply that method.
        # For this example, calling grid.shift(LEFT) would shift the
        # grid one unit to the left, but both of the previous calls to
        # "self.play" animate that motion.

        # The same applies for any method, including those setting colors.
        self.play(grid.animate.set_color(YELLOW))
        self.wait()
        self.play(grid.animate.set_submobject_colors_by_gradient(BLUE, GREEN))
        self.wait()
        self.play(grid.animate.set_height(TAU - MED_SMALL_BUFF))
        self.wait()

        # The method Mobject.apply_complex_function lets you apply arbitrary
        # complex functions, treating the points defining the mobject as
        # complex numbers.
        self.play(grid.animate.apply_complex_function(np.exp), run_time=5)
        self.wait()

        # Even more generally, you could apply Mobject.apply_function,
        # which takes in functions form R^3 to R^3
        self.play(
            grid.animate.apply_function(
                lambda p: [
                    p[0] + 0.5 * math.sin(p[1]),
                    p[1] + 0.5 * math.sin(p[0]),
                    p[2]
                ]
            ),
            run_time=5,
        )
        self.wait()

两个新概念的展开

  • .get_grid(n, m, height=...):在 Mobject.get_grid 中定义,返回一个新 mobject,包含原 mobject 的多份副本并按 n 列 m 行排成网格,height 指定整体高度。示例中用它把 π 符号铺成 10×10 的网格,作为后续形变的“测试布”。
  • .animate 语法:其语义是把 mobject 的某个“修改型方法”转换成动画——在对象当前状态与方法调用后的目标状态之间做插值。例如 grid.shift(LEFT) 会立即整体左移 1 个单位,而 self.play(grid.animate.shift(LEFT)) 则播放这段移动的动画。对任何方法都成立,包括 set_colorset_height 这类看起来与“位置”无关的方法。
  • apply_complex_function / apply_function:前者把定义 mobject 的点当作复数,应用任意复函数(示例中 np.exp 会产出著名的螺旋形变);后者接受 R^3 → R^3 的普通函数,示例中用正弦扰动生成波浪状形变。

四、TextExample:Text 的文字系统与逐字样式

完整代码:

class TextExample(Scene):
    def construct(self):
        # To run this scene properly, you should have "Consolas" font in your computer
        text = Text("Here is a text", font="Consolas", font_size=90)
        difference = Text(
            """
            The most important difference between Text and TexText is that\n
            you can change the font more easily, but can't use the LaTeX grammar
            """,
            font="Arial", font_size=24,
            # t2c is a dict that you can choose color for different text
            t2c={"Text": BLUE, "TexText": BLUE, "LaTeX": ORANGE}
        )
        VGroup(text, difference).arrange(DOWN, buff=1)
        self.play(Write(text))
        self.play(FadeIn(difference, UP))
        self.wait(3)

        fonts = Text(
            "And you can also set the font according to different words",
            font="Arial",
            t2f={"font": "Consolas", "words": "Consolas"},
            t2c={"font": BLUE, "words": GREEN}
        )
        fonts.set_width(FRAME_WIDTH - 1)
        slant = Text(
            "And the same as slant and weight",
            font="Consolas",
            t2s={"slant": ITALIC},
            t2w={"weight": BOLD},
            t2c={"slant": ORANGE, "weight": RED}
        )
        VGroup(fonts, slant).arrange(DOWN, buff=0.8)
        self.play(FadeOut(text), FadeOut(difference, shift=DOWN))
        self.play(Write(fonts))
        self.wait()
        self.play(Write(slant))
        self.wait()

文档对新类的总结:

  • Text:创建纯文字(不经过 LaTeX),可直接指定系统字体(font)。注意示例注释:要在本机正确运行,需要安装对应字体(如 Consolas、Arial)。
  • VGroup:把多个 mobject 组合成整体,.arrange(DOWN, buff=1) 表示按 DOWN 方向依次排列,间距为 buff
  • Write:书写效果动画;FadeIn/FadeOut:淡入/淡出动画,第二个位置参数表示淡入/淡出的方向。

t2c / t2f / t2s / t2w:逐词自定义样式

这是 Text 最实用的能力,四个字典参数分别对“子串”生效:

参数 含义 示例
t2c 子串 → 颜色 t2c={"font": BLUE}
t2f 子串 → 字体 t2f={"font": "Consolas"}
t2s 子串 → 斜体样式 t2s={"slant": ITALIC}
t2w 子串 → 字重 t2w={"weight": BOLD}

即可以为同一句话里的不同单词设置不同字体、颜色、斜体与粗细,Text 的实现位于 text_mobject.pyStringMobject 体系)。fonts.set_width(FRAME_WIDTH - 1) 则演示了把文字宽度撑满屏幕。

五、TexTransformExample:公式间的匹配变换

这是本文件中最体现 manim“把数学函数可视化为变换”理念的示例。文档版完整代码(使用 OldTexisolate 切分子 mobject):

class TexTransformExample(Scene):
    def construct(self):
        to_isolate = ["B", "C", "=", "(", ")"]
        lines = VGroup(
            # Passing in muliple arguments to Tex will result
            # in the same expression as if those arguments had
            # been joined together, except that the submobject
            # hierarchy of the resulting mobject ensure that the
            # Tex mobject has a subject corresponding to
            # each of these strings.  For example, the Tex mobject
            # below will have 5 subjects, corresponding to
            # the expressions [A^2, +, B^2, =, C^2]
            OldTex("A^2", "+", "B^2", "=", "C^2"),
            OldTex("A^2", "=", "C^2", "-", "B^2"),
            # Alternatively, you can pass in the keyword argument
            # "isolate" with a list of strings that should be out as
            # their own submobject.
            OldTex("A^2 = (C + B)(C - B)", isolate=["A^2", *to_isolate]),
            OldTex("A = \\sqrt{(C + B)(C - B)}", isolate=["A", *to_isolate])
        )
        lines.arrange(DOWN, buff=LARGE_BUFF)
        for line in lines:
            line.set_color_by_tex_to_color_map({
                "A": BLUE,
                "B": TEAL,
                "C": GREEN,
            })

        play_kw = {"run_time": 2}
        self.add(lines[0])
        self.play(
            TransformMatchingTex(
                lines[0].copy(), lines[1],
                path_arc=90 * DEG,
            ),
            **play_kw
        )
        self.wait()

        self.play(
            TransformMatchingTex(lines[1].copy(), lines[2]),
            **play_kw
        )
        self.wait()
        # If, however, we want the C^2 to go to C, and B^2 to go to B,
        # we can specify that with a key map.
        self.play(FadeOut(lines[2]))
        self.play(
            TransformMatchingTex(
                lines[1].copy(), lines[2],
                key_map={
                    "C^2": "C",
                    "B^2": "B",
                }
            ),
            **play_kw
        )
        self.wait()

        # Let the exponent "^2" transform into the square root symbol
        new_line2 = OldTex("A^2 = (C + B)(C - B)", isolate=["A", *to_isolate])
        new_line2.replace(lines[2])
        new_line2.match_style(lines[2])

        self.play(
            TransformMatchingTex(
                new_line2, lines[3],
                transform_mismatches=True,
            ),
            **play_kw
        )
        self.wait(3)
        self.play(FadeOut(lines, RIGHT))

        source = Text("the morse code", height=1)
        target = Text("here come dots", height=1)

        self.play(Write(source))
        self.wait()
        kw = {"run_time": 3, "path_arc": PI / 2}
        self.play(TransformMatchingShapes(source, target, **kw))
        self.wait()
        self.play(TransformMatchingShapes(target, source, **kw))
        self.wait()

文档对四个新类的总结:Tex(LaTeX 数学公式)、TexText(LaTeX 文字)、TransformMatchingTex(按 tex 子串的异同自动对齐变换)、TransformMatchingShapes(按点集形状相似性直接对齐变换)。

源码视角:匹配算法是怎么实现的

这些动画全部继承自 TransformMatchingPartsTransformMatchingShapes 是其别名,TransformMatchingTex 继承 TransformMatchingStrings),核心流程可概括为三步:

  1. 收集匹配对TransformMatchingStrings.matching_blocks(见 transform_matching_parts.py)先取用户显式指定的 key_map(如 {"C^2": "C"})和 matched_keys 作为最高优先级匹配,再对两侧符号子串列表使用 difflib.SequenceMatcher 反复找“最长公共子串”自动匹配——这就是“按 tex 相似性自动对齐”的底层算法。
  2. 按形状补齐find_pairs_with_matching_shapes 对尚未匹配的碎片两两调用 has_same_shape_as,形状相同的碎片走 match_animation(默认 Transform),否则走 mismatch_animation
  3. 处理剩余部分:源侧未匹配的碎片 FadeOutToPoint 到目标中心,目标侧未匹配的碎片 FadeInFromPoint 从源中心淡入。transform_mismatches=True 时,形状不一致的碎片之间也强制建立 Transform,于是示例中 ^2 指数可以“长”成 \sqrt 根号。

path_arc 参数让每个碎片沿圆弧路径旋转到位,文档注释解释这正是“重排公式”观感的来源。另外,isolate 参数(以及 Tex("A^2", "+", "B^2", ...) 的多参数写法)决定了子 mobject 的切分粒度,直接决定哪些部分能作为整体参与匹配——示例最后一幕先把 A^2 拆成独立的 A^2,再让 ^2 单独变换为根号,正是利用了这一点。

说明:仓库 example_scenes.py 中的同名场景使用了功能等价的 TexTransformMatchingStrings(配合 matched_keyskey_map={"2": R"\sqrt"} 写法),二者来自同一实现体系,可互换参考。

六、UpdatersExample:更新器(Updater)机制

Updater 是 manim 中“每帧自动执行某段逻辑”的机制,典型用途是让括号、标签实时跟随变形的对象。完整代码:

class UpdatersExample(Scene):
    def construct(self):
        square = Square()
        square.set_fill(BLUE_E, 1)

        # On all all frames, the constructor Brace(square, UP) will
        # be called, and the mobject brace will set its data to match
        # that of the newly constructed object
        brace = always_redraw(Brace, square, UP)

        text, number = label = VGroup(
            Text("Width = "),
            DecimalNumber(
                0,
                show_ellipsis=True,
                num_decimal_places=2,
                include_sign=True,
            )
        )
        label.arrange(RIGHT)

        # This ensures that the method deicmal.next_to(square)
        # is called on every frame
        always(label.next_to, brace, UP)
        # You could also write the following equivalent line
        # label.add_updater(lambda m: m.next_to(brace, UP))

        # If the argument itself might change, you can use f_always,
        # for which the arguments following the initial Mobject method
        # should be functions returning arguments to that method.
        # The following line ensures that decimal.set_value(square.get_y())
        # is called every frame
        f_always(number.set_value, square.get_width)
        # You could also write the following equivalent line
        # number.add_updater(lambda m: m.set_value(square.get_width()))

        self.add(square, brace, label)

        # Notice that the brace and label track with the square
        self.play(
            square.animate.scale(2),
            rate_func=there_and_back,
            run_time=2,
        )
        self.wait()
        self.play(
            square.animate.set_width(5, stretch=True),
            run_time=3,
        )
        self.wait()
        self.play(
            square.animate.set_width(2),
            run_time=3
        )
        self.wait()

        # In general, you can alway call Mobject.add_updater, and pass in
        # a function that you want to be called on every frame.  The function
        # should take in either one argument, the mobject, or two arguments,
        # the mobject and the amount of time since the last frame.
        now = self.time
        w0 = square.get_width()
        square.add_updater(
            lambda m: m.set_width(w0 * math.cos(self.time - now))
        )
        self.wait(4 * PI)

文档对新用法的总结:

  • always_redraw():每帧重建一个 mobject(示例中每帧重新构造 Brace(square, UP),使括号始终贴合当前方形宽度);
  • DecimalNumber:可变数字 mobject,拆成 Text 字符,数字变化时逐位刷新;
  • always(f, x):每帧执行 f(x)(x 为固定值);
  • f_always(f, g):每帧执行 f(g()),适用于参数本身也会变化的场景(示例中每帧把 square.get_width() 的最新值写入数字);
  • .add_updater():注册一个每帧调用的回调,回调可接收 1 个参数(mobject)或 2 个参数(mobject、距上帧的时间 dt);
  • .to_edge() / .center() / .set_y() 等定位方法按文档描述分别为靠边、居中、设置纵坐标。

源码视角:always / f_always / always_redraw 的实现

三者都在 mobject_update_utils.py 中,本质都是 add_updater 的语法糖:

  • always(method, *args):断言参数是 Mobject 方法,取出其未绑定函数 func 与宿主 mobject,注册 lambda m: func(m, *args)——即文档中“等价于 label.add_updater(lambda m: m.next_to(brace, UP))”的来源;
  • f_always(method, *arg_generators):参数是“生成器函数”,updater 内先逐个调用生成器取到实参再执行方法,故能处理“参数随时间变化”的情况;
  • always_redraw(func, *args):立即调用一次 func 得到 mobject,再注册 lambda m: mob.become(func(*args))——每帧用 become 把 mobject 的数据替换为全新构造结果,这就是括号能“贴合变形”的机制。

此外,Mobject 还提供 mob.always.xxx / mob.f_always.xxx 的属性式写法(如 label.always.next_to(brace, UP)),由 Mobject 中的 _UpdaterBuilder / _FunctionalUpdaterBuilder 实现:__getattr__ 捕获任意方法名并自动包装成 updater。仓库 example_scenes.py 中的 UpdatersExample 还演示了时间驱动型 updater:lambda m, dt: m.rotate(dt) 按帧间时间增量旋转对象。

七、CoordinateSystemExample:坐标系与坐标映射

完整代码:

class CoordinateSystemExample(Scene):
    def construct(self):
        axes = Axes(
            # x-axis ranges from -1 to 10, with a default step size of 1
            x_range=(-1, 10),
            # y-axis ranges from -2 to 2 with a step size of 0.5
            y_range=(-2, 2, 0.5),
            # The axes will be stretched so as to match the specified
            # height and width
            height=6,
            width=10,
            # Axes is made of two NumberLine mobjects.  You can specify
            # their configuration with axis_config
            axis_config={
                "stroke_color": GREY_A,
                "stroke_width": 2,
            },
            # Alternatively, you can specify configuration for just one
            # of them, like this.
            y_axis_config={
                "include_tip": False,
            }
        )
        # Keyword arguments of add_coordinate_labels can be used to
        # configure the DecimalNumber mobjects which it creates and
        # adds to the axes
        axes.add_coordinate_labels(
            font_size=20,
            num_decimal_places=1,
        )
        self.add(axes)

        # Axes descends from the CoordinateSystem class, meaning
        # you can call call axes.coords_to_point, abbreviated to
        # axes.c2p, to associate a set of coordinates with a point,
        # like so:
        dot = Dot(fill_color=RED)
        dot.move_to(axes.c2p(0, 0))
        self.play(FadeIn(dot, scale=0.5))
        self.play(dot.animate.move_to(axes.c2p(3, 2)))
        self.wait()
        self.play(dot.animate.move_to(axes.c2p(5, 0.5)))
        self.wait()

        # Similarly, you can call axes.point_to_coords, or axes.p2c
        # print(axes.p2c(dot.get_center()))

        # We can draw lines from the axes to better mark the coordinates
        # of a given point.
        # Here, the always_redraw command means that on each new frame
        # the lines will be redrawn
        h_line = always_redraw(lambda: axes.get_h_line(dot.get_left()))
        v_line = always_redraw(lambda: axes.get_v_line(dot.get_bottom()))

        self.play(
            ShowCreation(h_line),
            ShowCreation(v_line),
        )
        self.play(dot.animate.move_to(axes.c2p(3, -2)))
        self.wait()
        self.play(dot.animate.move_to(axes.c2p(1, 1)))
        self.wait()

        # If we tie the dot to a particular set of coordinates, notice
        # that as we move the axes around it respects the coordinate
        # system defined by them.
        f_always(dot.move_to, lambda: axes.c2p(1, 1))
        self.play(
            axes.animate.scale(0.75).to_corner(UL),
            run_time=2,
        )
        self.wait()
        self.play(FadeOut(VGroup(axes, dot, h_line, v_line)))

要点解析:

  • Axes 的构造参数:x_range / y_range 指定取值范围与步长(三元素形式 (min, max, step)),height / width 指定整体尺寸(坐标系会拉伸以匹配);
  • axis_config 把参数透传给内部两条 NumberLineAxes 由两条 NumberLine 组成),y_axis_config 只针对 y 轴,例如 include_tip=False 去掉箭头;
  • add_coordinate_labels(font_size=20, num_decimal_places=1) 的 kwargs 用来配置其内部创建的 DecimalNumber
  • axes.c2p(x, y)coords_to_point 的缩写)把数学坐标映射为屏幕点,axes.p2c(point) 反之;
  • always_redraw(lambda: axes.get_h_line(...)) 每帧重绘水平/垂直参考线,使参考线始终从坐标轴指向动点;
  • 末段用 f_always(dot.move_to, lambda: axes.c2p(1, 1)) 把点“绑定”到坐标 (1,1):即使整组坐标轴被 scale(0.75).to_corner(UL) 搬走缩放,点依然跟随坐标系走——这是“点跟随坐标系而非屏幕”的关键技巧;
  • 文档注释提示还可尝试 ThreeDAxesNumberPlaneComplexPlane,相关实现位于 coordinate_systems.py

八、GraphExample:在坐标系上绘制函数图像

完整代码:

class GraphExample(Scene):
    def construct(self):
        axes = Axes((-3, 10), (-1, 8))
        axes.add_coordinate_labels()

        self.play(Write(axes, lag_ratio=0.01, run_time=1))

        # Axes.get_graph will return the graph of a function
        sin_graph = axes.get_graph(
            lambda x: 2 * math.sin(x),
            color=BLUE,
        )
        # By default, it draws it so as to somewhat smoothly interpolate
        # between sampled points (x, f(x)).  If the graph is meant to have
        # a corner, though, you can set use_smoothing to False
        relu_graph = axes.get_graph(
            lambda x: max(x, 0),
            use_smoothing=False,
            color=YELLOW,
        )
        # For discontinuous functions, you can specify the point of
        # discontinuity so that it does not try to draw over the gap.
        step_graph = axes.get_graph(
            lambda x: 2.0 if x > 3 else 1.0,
            discontinuities=[3],
            color=GREEN,
        )

        # Axes.get_graph_label takes in either a string or a mobject.
        # If it's a string, it treats it as a LaTeX expression.  By default
        # it places the label next to the graph near the right side,
        # and has it match the color of the graph
        sin_label = axes.get_graph_label(sin_graph, "\\sin(x)")
        relu_label = axes.get_graph_label(relu_graph, Text("ReLU"))
        step_label = axes.get_graph_label(step_graph, Text("Step"), x=4)

        self.play(
            ShowCreation(sin_graph),
            FadeIn(sin_label, RIGHT),
        )
        self.wait(2)
        self.play(
            ReplacementTransform(sin_graph, relu_graph),
            FadeTransform(sin_label, relu_label),
        )
        self.wait()
        self.play(
            ReplacementTransform(relu_graph, step_graph),
            FadeTransform(relu_label, step_label),
        )
        self.wait()

        parabola = axes.get_graph(lambda x: 0.25 * x**2)
        parabola.set_stroke(BLUE)
        self.play(
            FadeOut(step_graph),
            FadeOut(step_label),
            ShowCreation(parabola)
        )
        self.wait()

        # You can use axes.input_to_graph_point, abbreviated
        # to axes.i2gp, to find a particular point on a graph
        dot = Dot(fill_color=RED)
        dot.move_to(axes.i2gp(2, parabola))
        self.play(FadeIn(dot, scale=0.5))

        # A value tracker lets us animate a parameter, usually
        # with the intent of having other mobjects update based
        # on the parameter
        x_tracker = ValueTracker(2)
        f_always(
            dot.move_to,
            lambda: axes.i2gp(x_tracker.get_value(), parabola)
        )

        self.play(x_tracker.animate.set_value(4), run_time=3)
        self.play(x_tracker.animate.set_value(-2), run_time=3)
        self.wait()

关键 API 归纳(均在 Axes/CoordinateSystem 体系内,实现见 coordinate_systems.py):

API 作用 示例细节
axes.get_graph(f, **kwargs) 采样 f(x) 并连线成图 默认平滑插值;折线函数用 use_smoothing=False(ReLU 例);断点函数传 discontinuitie s=[3] 避免跨缺口连线
axes.get_graph_label(graph, label, x=...) 生成图例标签 字符串按 LaTeX 处理,mobject 则直接用;默认放在图像右端并继承图像颜色
axes.i2gp(x, graph) input_to_graph_point 缩写,求函数图上的点 用于放置动点
ValueTracker 可动画的参数载体 x_tracker.animate.set_value(...) 动画化数值变化,配合 f_always 让动点沿抛物线滑动

动点沿曲线滑动的组合技值得单独记住:ValueTracker 持有自变量 x 的当前值 → f_always(dot.move_to, lambda: axes.i2gp(x_tracker.get_value(), parabola)) 每帧重算点位置 → self.play(x_tracker.animate.set_value(...)) 提供动画驱动。ValueTracker 定义于 value_tracker.py

九、SurfaceExample:三维曲面、相机与光源

这是文档中的三维场景示例。注意文档版使用 CONFIG = {"camera_class": ThreeDCamera} 指定相机,而仓库 example_scenes.py 中的等价实现直接继承 ThreeDScene 并使用 self.frame,两种写法都可用。文档版完整代码:

class SurfaceExample(Scene):
    CONFIG = {
        "camera_class": ThreeDCamera,
    }

    def construct(self):
        surface_text = Text("For 3d scenes, try using surfaces")
        surface_text.fix_in_frame()
        surface_text.to_edge(UP)
        self.add(surface_text)
        self.wait(0.1)

        torus1 = Torus(r1=1, r2=1)
        torus2 = Torus(r1=3, r2=1)
        sphere = Sphere(radius=3, resolution=torus1.resolution)
        # You can texture a surface with up to two images, which will
        # be interpreted as the side towards the light, and away from
        # the light.  These can be either urls, or paths to a local file
        # in whatever you've set as the image directory in
        # the custom_config.yml file

        # day_texture = "EarthTextureMap"
        # night_texture = "NightEarthTextureMap"
        day_texture = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Whole_world_-_land_and_oceans.jpg/1280px-Whole_world_-_land_and_oceans.jpg"
        night_texture = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/The_earth_at_night.jpg/1280px-The_earth_at_night.jpg"

        surfaces = [
            TexturedSurface(surface, day_texture, night_texture)
            for surface in [sphere, torus1, torus2]
        ]

        for mob in surfaces:
            mob.shift(IN)
            mob.mesh = SurfaceMesh(mob)
            mob.mesh.set_stroke(BLUE, 1, opacity=0.5)

        # Set perspective
        frame = self.camera.frame
        frame.set_euler_angles(
            theta=-30 * DEG,
            phi=70 * DEG,
        )

        surface = surfaces[0]

        self.play(
            FadeIn(surface),
            ShowCreation(surface.mesh, lag_ratio=0.01, run_time=3),
        )
        for mob in surfaces:
            mob.add(mob.mesh)
        surface.save_state()
        self.play(Rotate(surface, PI / 2), run_time=2)
        for mob in surfaces[1:]:
            mob.rotate(PI / 2)

        self.play(
            Transform(surface, surfaces[1]),
            run_time=3
        )

        self.play(
            Transform(surface, surfaces[2]),
            # Move camera frame during the transition
            frame.animate.increment_phi(-10 * DEG),
            frame.animate.increment_theta(-20 * DEG),
            run_time=3
        )
        # Add ambient rotation
        frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))

        # Play around with where the light is
        light_text = Text("You can move around the light source")
        light_text.move_to(surface_text)
        light_text.fix_in_frame()

        self.play(FadeTransform(surface_text, light_text))
        light = self.camera.light_source
        self.add(light)
        light.save_state()
        self.play(light.animate.move_to(3 * IN), run_time=5)
        self.play(light.animate.shift(10 * OUT), run_time=5)

        drag_text = Text("Try moving the mouse while pressing d or s")
        drag_text.move_to(light_text)
        drag_text.fix_in_frame()

        self.play(FadeTransform(light_text, drag_text))
        self.wait()

文档对该场景的要点总结(配合源码理解):

  • 三维曲面SphereTorus(r1, r2) 生成参数化曲面,TexturedSurface(surface, day_texture, night_texture) 贴上最多两张贴图,分别对应“朝向光源面”与“背向光源面”;贴图可以是 URL,也可以是 custom_config.yml 指定 image 目录下的本地文件。相关实现位于 surface.py
  • SurfaceMesh:为曲面生成线框网格,set_stroke(BLUE, 1, opacity=0.5) 设置线框样式,ShowCreation(surface.mesh, lag_ratio=0.01) 让网格逐线绘制。
  • .fix_in_frame():使对象不随视角变化而改变,始终显示在屏幕固定位置(示例中的提示文字)。
  • 相机控制self.camera.frame 是三维相机的取景框 mobject,set_euler_angles(theta, phi) 设置初始视角;由于它本身是 mobject,可以用 frame.animate.increment_phi(...) / increment_theta(...) 动画化改变视角,甚至用时间型 updater lambda m, dt: m.increment_theta(-0.1 * dt) 添加“环境旋转”。
  • 光源self.camera.light_source 也是 mobject,可 save_state() 后用 animate.move_to/shift 移动,实时改变曲面光照。
  • 文档结尾提示可在渲染窗口中按住 d(改变视角)或 s(平移)拖动鼠标体验交互。

十、OpeningManimExample:二维综合场景

作为压轴示例,该场景综合了文字、网格、矩阵、复平面与函数变换,完整代码:

class OpeningManimExample(Scene):
    def construct(self):
        intro_words = Text("""
            The original motivation for manim was to
            better illustrate mathematical functions
            as transformations.
        """)
        intro_words.to_edge(UP)

        self.play(Write(intro_words))
        self.wait(2)

        # Linear transform
        grid = NumberPlane((-10, 10), (-5, 5))
        matrix = [[1, 1], [0, 1]]
        linear_transform_words = VGroup(
            Text("This is what the matrix"),
            IntegerMatrix(matrix, include_background_rectangle=True),
            Text("looks like")
        )
        linear_transform_words.arrange(RIGHT)
        linear_transform_words.to_edge(UP)
        linear_transform_words.set_stroke(BLACK, 10, background=True)

        self.play(
            ShowCreation(grid),
            FadeTransform(intro_words, linear_transform_words)
        )
        self.wait()
        self.play(grid.animate.apply_matrix(matrix), run_time=3)
        self.wait()

        # Complex map
        c_grid = ComplexPlane()
        moving_c_grid = c_grid.copy()
        moving_c_grid.prepare_for_nonlinear_transform()
        c_grid.set_stroke(BLUE_E, 1)
        c_grid.add_coordinate_labels(font_size=24)
        complex_map_words = TexText("""
            Or thinking of the plane as $\\mathds{C}$,\\
            this is the map $z \\rightarrow z^2$
        """)
        complex_map_words.to_corner(UR)
        complex_map_words.set_stroke(BLACK, 5, background=True)

        self.play(
            FadeOut(grid),
            Write(c_grid, run_time=3),
            FadeIn(moving_c_grid),
            FadeTransform(linear_transform_words, complex_map_words),
        )
        self.wait()
        self.play(
            moving_c_grid.animate.apply_complex_function(lambda z: z**2),
            run_time=6,
        )
        self.wait(2)

该场景串起了前几节的大部分能力:

  1. 线性变换演示NumberPlane((-10, 10), (-5, 5)) 建立网格;IntegerMatrix(matrix, include_background_rectangle=True) 渲染带背景矩形的矩阵;grid.animate.apply_matrix(matrix) 让整张网格按矩阵 [[1,1],[0,1]] 做切变——直观展示“矩阵即变换”;set_stroke(BLACK, 10, background=True) 给文字加黑色背景描边以便在网格上阅读。
  2. 复函数演示ComplexPlane() 建立复平面;prepare_for_nonlinear_transform() 预先加密网格采样,使非线性变换时网格不变形撕裂;复制体 moving_c_grid 执行 apply_complex_function(lambda z: z**2),静态的 c_grid 保持原样作参照,这正是可视化 z → z² 映射的标准手法。
  3. 动画组合ShowCreationFadeTransformFadeIn/FadeOutWrite 在同一 self.play(...) 中并行播放,展示 manim 的多动画并发能力。

文档在此场景后总结:看完这些场景,你已经掌握了 manim 的大部分用法;更多示例可参考 3b1b 的开源视频代码仓库(3b1b/videos,在 example_scenes.py 尾部注释中有提及)。

十一、能力地图:从九个场景提炼的学习路径

把文档九个场景按知识点归类,可作为自学路线:

场景 核心知识点 关键源码
InteractiveDevelopment self.embed() 交互终端、窗口快捷键、鼠标响应 scene.pyscene_embed.py
AnimatingMethods .animate 语法、.get_grid()apply_complex_function/apply_function mobject.py
TextExample Text 的 t2c/t2f/t2s/t2w、VGroup.arrange text_mobject.py
TexTransformExample Tex 子对象切分(多参数/isolate)、TransformMatchingTexkey_maptransform_mismatchesTransformMatchingShapes transform_matching_parts.py
UpdatersExample always/f_always/always_redraw/add_updaterDecimalNumber mobject_update_utils.py
CoordinateSystemExample Axes 配置、c2p/p2c、参考线重绘 coordinate_systems.py
GraphExample get_graph/get_graph_label/i2gpValueTracker value_tracker.py
SurfaceExample 三维曲面、贴图、线框、相机帧与光源动画、fix_in_frame surface.py
OpeningManimExample NumberPlane/ComplexPlane/IntegerMatrix、矩阵与复函数变换的综合应用 coordinate_systems.py

实际使用时,先按 manimgl example_scenes.py <SceneName> 逐个跑通这些场景,在 self.embed() 终端里对每一段代码做局部修改观察效果,再对照上文源码链接理解行为背后的机制,即可把这些官方示例中的技巧迁移到自己的场景文件中。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384