Manim 的 updater 是什么?add_updater 和 always_redraw 有什么区别
2026 年 11 月 8 日 · 极坐标⋅XYZ
Manim 的 updater 是什么?add_updater 和 always_redraw 有什么区别
updater 是挂在物体上的一个函数,动画播放时 Manim 每渲染一帧就调它一次,用来让物体跟随、联动或自己运动。add_updater 是把这个函数挂到一个已有的物体上,每帧在原物体上改位置、数值、颜色;always_redraw 是它的快捷写法,你给一个返回图形的函数,它每帧把这个图形整个重新造一遍。物体只是位置在变,用 add_updater;物体的形状本身要跟着别的东西变(连两个动点的线、会伸缩的花括号),用 always_redraw。
Manim 是 Grant Sanderson 为 3Blue1Brown 频道开发的数学动画引擎,画面里的点、线、公式都是 Python 对象,动画由你用代码一步步描述,渲染时再逐帧合成视频。现在多数人用的是社区维护的 Manim Community,2026 年的最新版本是 0.21.0,本文的代码按这一版写。还没接触过 Manim 的话,可以先看这篇介绍;想系统学下去,极坐标⋅XYZ 上有一套按 0.21.0 做的 2026 年最新 Manim 中文教程,updater 单独有一章。
下面的完整代码都能在极坐标⋅XYZ 的工作区里直接运行,点代码块上面的「在工作区中打开」即可,工作区装的是同一版本,不用在电脑上装环境。
最小可运行的例子:两个写法并排用
画面里有两个点,蓝点 A 不动,黄点 B 会被挪走。B 旁边有个标签 "B",A 和 B 之间有一条连线。标签只是跟着 B 换位置,所以用 add_updater;连线两端都在变,所以用 always_redraw:
from manim import *
class UpdaterVsRedraw(Scene):
def construct(self):
a = Dot(LEFT * 3, color=BLUE)
b = Dot(RIGHT * 3, color=YELLOW)
label = Text("B", font_size=32, color=YELLOW)
label.next_to(b, UP)
# add_updater:每帧在原来的 label 上改位置
label.add_updater(lambda m: m.next_to(b, UP))
# always_redraw:每帧用 a、b 此刻的位置重新造一条线
link = always_redraw(
lambda: Line(a.get_center(), b.get_center(), color=GREY_B)
)
self.add(link, a, b, label)
self.play(b.animate.shift(UP * 2 + LEFT * 2), run_time=2)
self.play(a.animate.shift(DOWN * 1.5), run_time=2)
self.wait()
两个点怎么动,标签和连线都跟得上。这段代码的写法和课程里「行星—卫星」那一节是同一套:卫星用 updater 跟着行星,行星到卫星的连线用 always_redraw。
再看一个例子:一个 ValueTracker 驱动三件事
下面用一个 ValueTracker 同时驱动三件事:矩形宽度变化,花括号按矩形重造,数字按数值重造。
from manim import *
class RedrawWithTracker(Scene):
def construct(self):
w = ValueTracker(1)
rect = always_redraw(
lambda: Rectangle(width=w.get_value(), height=1.5, color=BLUE)
.set_fill(BLUE, 0.4)
.shift(UP * 0.5)
)
brace = always_redraw(lambda: Brace(rect, DOWN))
number = always_redraw(
lambda: DecimalNumber(w.get_value(), num_decimal_places=1)
.next_to(brace, DOWN)
)
self.add(rect, brace, number)
self.play(w.animate.set_value(5), run_time=3)
self.play(w.animate.set_value(2), run_time=2)
self.wait()
矩形的宽度、花括号的宽度、数字的内容全都由 w 决定,你只拧 w 这一个旋钮。花括号的形状随宽度变,数字的内容随数值变,都是「整个重造」更自然的场景。
区别在哪:改原物体,还是每帧换新物体
两者的差别只有一个:每一帧,Manim 拿到的是同一个对象,还是一个新造的对象。
add_updater(func):func 的第一个参数是被挂的物体本身,你在函数里直接修改它(m.next_to(...)、m.move_to(...)、m.set_value(...))。物体始终是原来那个,它的样式、大小、之前对它做过的设置都保留,改的只是你在函数里写的那几项。函数还可以多收一个 dt 参数,表示距上一帧过了多少秒,位移乘上 dt 就是「每秒走多少」,和帧率无关。
always_redraw(func):func 不收参数,返回一个全新的图形。Manim 每帧调一次 func,把返回的新图形的样子换到画面上的那个物体里。所以图形的一切(形状、颜色、大小)都由 func 里的代码决定,每帧从头算一遍。
add_updater |
always_redraw |
|
|---|---|---|
| 函数怎么写 | lambda m: ...,修改传进来的 m |
lambda: ...,返回一个新图形 |
| 每帧发生什么 | 原物体被就地修改 | 图形按函数重新造一遍 |
| 适合 | 位置、数值、朝向在变,形状没变 | 形状本身随别的东西变 |
能用 dt |
能,第二个参数 | 不能,函数不带参数 |
| 返回值 | 无,直接改 m |
返回一个新图形,也会被当作要显示的物体 |
| 开销 | 小 | 大一些,每帧新建图形 |
可以这样理解:always_redraw 就是「一个 updater 加一个重造」的打包。把它展开,大致是先调一次函数造出图形,再给这个图形挂一个 updater,每帧用函数的新结果替换它的内容。所以它能做的事,手写 add_updater 都能做,只是要多写几行。
什么时候用哪个
用 add_updater:
- 标签、箭头跟着一个动点走,只改位置(上一篇讲的
next_to不跟随就是这种情况)。 - 要按
dt匀速运动,比如让一个点每秒转多少度、让ValueTracker每帧加一点。 - 要读取别的物体、再更新自己的内容,例如读出点的坐标、更新标签上的数字。
- 想保留物体已有的样式,不希望每帧被重置。
用 always_redraw:
- 连接两个动点的线段。起点终点都在变,线的长度和方向每帧不同。
- 随
ValueTracker变大变小的图形,例如上面那个宽度由数值决定的矩形,以及贴在它下面、跟着量出宽度的花括号。 - 图形的构造本身要依赖别的东西(比如曲线下的面积随积分上限变化),写成一个返回图形的函数最直接。
为什么是这样
updater 本质是挂在物体上的一个函数列表。场景每播一帧,会先把时间推进一步,再把每个物体上挂着的 updater 依次调用一遍,最后才渲染画面。所以 updater 里读到的永远是「这一帧刚算完」的最新状态,画面上的关系每帧都被重新维持一次。
next_to、move_to 这类布局方法只在被调用的那一行算一次,它们并不会记住「我要一直在谁旁边」。updater 把这一次性的摆放变成每帧都执行的承诺,这是它和普通布局调用的根本区别,也是课程里 updater 这一章要讲的核心:让关系持续生效。
always_redraw 的函数在你调用它时会立刻执行一次,用来造出初始图形;之后每帧再执行。所以你不需要像手写 updater 那样先摆一次位置。手写 add_updater 时则没有这一步,挂上去的函数不会自动先执行(想立刻执行一次,可以传 call_updater=True),第一帧之前要自己先摆好。
常见写错的地方
对 always_redraw 的结果手动改样式
rect = always_redraw(lambda: Rectangle(width=w.get_value(), color=BLUE))
rect.set_color(RED) # 下一帧就被函数里的 BLUE 盖回去
always_redraw 的图形每帧都按函数重造,函数外面对它做的修改撑不过一帧。想改颜色,就改函数里的颜色:
rect = always_redraw(lambda: Rectangle(width=w.get_value(), color=RED))
用 always_redraw 去做只改位置的事
label = always_redraw(lambda: MathTex(r"\triangle ABC").next_to(tri, RIGHT))
位置在变,形状没变,却每帧新建一个 MathTex,要重新排一遍公式,这是 updater 里最费时间的操作之一。位置的事用 add_updater:
label = MathTex(r"\triangle ABC").next_to(tri, RIGHT)
label.add_updater(lambda m: m.next_to(tri, RIGHT))
在 for 循环里写 lambda,所有物体都跟着最后一个
for dot, label in zip(dots, labels):
label.add_updater(lambda m: m.next_to(dot, UP))
三个标签全叠到最后一个点上。原因在 Python:lambda 里的 dot 等到被调用时才取值,那时循环已经结束。用默认参数把当前的 dot 留住:
for dot, label in zip(dots, labels):
label.add_updater(lambda m, d=dot: m.next_to(d, UP))
always_redraw 放进循环里也是同一个道理,函数里引用的循环变量要用默认参数固定。
以为 wait 期间所有 updater 都在跑
不带 dt 的 updater(包括 always_redraw)在 self.wait() 期间,如果场景里没有任何带 dt 的 updater,Manim 会把这段当成静止画面,不会调用它们。这个现象的细节在前一篇里讲过,要在 wait 里看到变化,就让物体通过 self.play(...) 去动,或者给场景里某个物体挂一个带 dt 的 updater。
常见追问(FAQ)
Q:怎么把 updater 撤掉?
A:mob.remove_updater(func) 摘掉指定的那个函数,mob.clear_updaters() 清掉这个物体上的全部 updater。摘掉之后物体停在当前位置,不再跟随。always_redraw 返回的物体也是用 updater 实现的,对它调 clear_updaters() 就会停止重绘。
Q:updater 里的 dt 是什么?
A:两帧之间的时间间隔,单位是秒。让物体每秒转 2 弧度,就写 m.rotate(2 * dt),这样不同帧率下走得一样快。直接 m.shift(RIGHT * 0.1) 的话,每帧走 0.1,帧率越高走得越快。
Q:always_redraw 和 ValueTracker 是什么关系?
A:ValueTracker 是一个能读能拨的数值,always_redraw 是让图形依赖这个数值。两者配合就是:你只动 ValueTracker,所有依赖它的图形自己重造到新状态。
Q:updater 会拖慢渲染吗?
A:每帧多调一次函数,一般的 next_to、move_to 感觉不出来。慢的是在 updater 或 always_redraw 里每帧新建 MathTex 这类重对象,能少建就少建。
Q:物体必须 self.add 过,updater 才会生效吗?
A:是。只挂了 updater、没加进场景的物体,Manim 不会去更新它。
这个知识点在教程《ValueTracker 与 Updater》里有配套视频和可改的代码,课里用一个行星—卫星的例子把 add_updater、always_redraw 和 ValueTracker 串在一起;想直接动手,打开工作区。
来源:极坐标⋅XYZ · jizuobiao.xyz · 更新于 2026-11-08 · 基于 Manim Community 0.21.0