Manim 里物体互相遮挡怎么办?z_index 和添加顺序谁说了算
2026 年 10 月 6 日 · 极坐标⋅XYZ
Manim 里物体互相遮挡怎么办?z_index 和添加顺序谁说了算
两个物体重叠时,Manim 先比 z_index,数字大的画在上面;z_index 一样,再看谁后加进场景,后加的在上面。每个物体的 z_index 默认是 0,所以什么都不设的时候,看到的就是「后 add 的盖住先 add 的」。想让某个物体压在上面,写 mob.set_z_index(1);想让它垫到最底下,给一个负数,比如 mob.set_z_index(-1)。
Manim 是一个用 Python 写数学动画的开源引擎,最早是 Grant Sanderson 给自己的频道 3Blue1Brown 做视频用的。画面里的圆、坐标系、公式各是一个对象,代码把它们一个个加进场景,Manim 再一帧一帧画出来。它没有图层面板,谁压着谁全由代码决定。现在通用的是社区维护的 Manim Community,2026 年的最新版本是 0.21.0,本文的结论是 2026 年 10 月在这一版上一条条渲出画面、读重叠处的像素颜色核对过的。还不熟悉 Manim 的话,可以先看这篇介绍;想从头系统学,2026 年最新的 Manim 中文教程在极坐标⋅XYZ 上有,从第一课看起就行。
下面三段完整代码可以在极坐标⋅XYZ 的工作区里直接运行,点代码块上面的「在工作区中打开」就行。工作区里也是 Manim 0.21.0,电脑上不用装 Python 和 LaTeX。
三个图形叠在一起,先不设,再设
一个红圆、一个青色三角、一个紫色方块,位置互相重叠。先按这个顺序加进场景,停一秒,再给圆和三角设上 z_index:
from manim import *
class WhoIsOnTop(Scene):
def construct(self):
circle = Circle(radius=1.2, color=RED).set_fill(RED, 1).move_to([-1, 0, 0])
triangle = Triangle(color=TEAL).set_fill(TEAL, 1).scale(1.6)
square = Square(side_length=2.2, color=PURPLE).set_fill(PURPLE, 1).move_to([1, 0, 0])
# 三个都没设 z_index(默认 0),后 add 的盖住先 add 的
self.add(circle, triangle, square)
self.wait()
# 不动添加顺序,只改 z_index:数字大的在上面
circle.set_z_index(2)
triangle.set_z_index(1)
self.wait()
前一秒里,方块在最上面,三角压着圆。我们在画面上取了三个点看颜色:圆和三角重叠的地方是青色,三角和方块重叠的地方是紫色,三个图形都盖到的那一点也是紫色。
后一秒整个反过来。同样三个点,颜色变成了红、青、红:圆在最上面,三角居中,方块到了最底下。这中间没有重新 add,也没有把谁移走,只改了两个数字。

set_z_index 可以在 add 之前调,也可以像上面这样在 add 之后调,下一帧就按新的数字画。数字不一定是正整数,我们试过 0.5 和 -1 都认。
背后只有一次排序
Manim 0.21.0 画每一帧之前,会把场景里的东西整理成一张表,这段逻辑在 manim/utils/family.py 里,去掉无关的部分大致是:
extracted_mobjects = remove_list_redundancies(
list(it.chain(*(m.get_family() for m in mobjects))),
)
if use_z_index:
return sorted(extracted_mobjects, key=lambda m: m.z_index)
第一步是摊平。mobjects 是场景里的物体,按添加顺序排着。每个物体连同它的成员、成员的成员(Manim 管这叫 family)依次展开,接成一张长表。一个 VGroup 里装着三个方块,展开后就是组自己加三个方块,一共四项。
第二步是按 z_index 从小到大排序。表里靠前的先画,靠后的后画,后画的盖住先画的。
Python 的 sorted 是稳定排序,z_index 相同的项,排完之后彼此的先后和排之前一样。所以「z_index 相同看添加顺序」其实是这次排序自带的结果,Manim 没有为它另写规则。
要注意的是,排的是摊平之后的表。组在这张表里只是其中一项,组里的每个成员各自带着自己的 z_index 参加排序,不会因为同属一个组就挨在一起。我们试过这样一个场景:g1 = VGroup(红, 橙),g2 = VGroup(蓝, 绿),只给红设了 z_index = 1,然后 self.add(g1, g2)。打印出来的绘制顺序是橙、蓝、绿、红。红是最先加进来的,却画在最后,压住了后加的 g2。

想自己看这张表,在 construct 里加一行:
print([(type(m).__name__, m.z_index) for m in self.get_mobject_family_members()])
后进场的底框,用负数垫下去
z_index 最常见的用处是这种情况:文字已经在画面上了,后面想给它加一个实心的底框。底框是后进场的,按添加顺序它在上面,会把文字整个盖掉。
from manim import *
class BoxBehindText(Scene):
def construct(self):
left = Text("z_index = 0").move_to([-3.3, 0, 0])
right = Text("z_index = -1").move_to([3.3, 0, 0])
self.add(left, right)
self.wait(0.5)
# 两个实心底框都比文字后进场,区别只在右边那个设了 z_index=-1
box_l = SurroundingRectangle(left, buff=0.3).set_fill(BLUE_E, 1)
box_r = SurroundingRectangle(right, buff=0.3).set_fill(BLUE_E, 1).set_z_index(-1)
self.play(FadeIn(box_l), FadeIn(box_r))
self.wait()
播完之后,左边只剩一块黄边、深蓝底的框,字看不见了。我们按 1280×720 渲出来数过左半边的白色像素,底框进场前是 1764 个,进场后是 0 个。右边的字还在,白色像素前后是 1651 和 1658,底框垫在了字的下面。
背景网格、坐标系后面的色块、给公式加的高亮底色,都是同一个写法:给要垫底的那个东西一个负的 z_index,什么时候加进场景就不用管了。反过来,一个标签要一直浮在所有东西上面,给它一个比别人都大的数。
z_index 在动画进行的过程中也一直算数。上面的 FadeIn 淡入期间,右边的字始终在框上面。我们另外试过:红色方块 z_index = 1 停着不动,一个后加的蓝色方块滑过来叠到它身上,重叠处从头到尾都是红色。
组的 z_index,设的是每个成员
对一个 VGroup 调 set_z_index(1),Manim 会把组里每个成员、成员的成员都设成 1。参数里有个 family=True,默认就是这样。
这是调用那一刻做的事,组不会记住「我的成员都该是 1」。之后再加进组的成员,z_index 还是它自己原来的值:
from manim import *
class LateMember(Scene):
def construct(self):
plate = RoundedRectangle(width=5, height=2, corner_radius=0.2, color=BLUE).set_fill(BLUE_E, 1)
card = VGroup(plate).set_z_index(1)
label = Text("label")
card.add(label) # 后加进组的成员,z_index 还是 0
self.add(card)
self.wait()
card.set_z_index(1) # 再设一次,这回 label 也是 1 了
self.wait()
前一秒画面上只有一块蓝色的底板,label 这几个字被它盖住了,卡片范围内一个白色像素都没有。label 明明是后加进组的,可它的 z_index 是 0,底板是 1,排序之后它排到了底板前面,先画。后一秒字出来了,因为又调了一次 card.set_z_index(1),两个成员都是 1,回到按组内顺序排,label 在后。
所以往一个设过 z_index 的组里加东西,要么加完再对整个组调一次 set_z_index,要么给新成员单独设。
动画里什么时候换层
z_index 是物体身上的一个属性,改了就改了,没有中间值。放进动画里,有两个地方和直觉不太一样。
第一个是 .animate.set_z_index(1)。它能用,但换层发生在动画播完的时候,播的过程中不变。我们让下面的红方块 self.play(red.animate.set_z_index(1), run_time=1),这一秒里重叠处一直是蓝色,播完才跳成红色。想让它在某个时刻换层,直接在两个 play 之间写 red.set_z_index(1) 更清楚,.animate 在这里没有额外的好处。
第二个是变形。Transform(a, b) 把 a 变成 b 的样子,场景里留下的还是 a,z_index 也还是 a 原来的值,b 身上设的 z_index 带不过来。ReplacementTransform(a, b) 播完后场景里换成了 b,从那一刻起用的是 b 的 z_index。如果两个数不一样,最后一帧会跳一下。我们试的是红方块 z_index = 3 变成一个没设 z_index 的黄方块,旁边有个蓝方块和它重叠:变形过程中它一直在蓝方块上面,颜色从红渐变到黄,播完的瞬间被蓝方块盖住了。
target = Square(color=YELLOW).set_fill(YELLOW, 1)
target.set_z_index(a.z_index) # 目标和原物体用同一个 z_index,播完就不会跳
self.play(ReplacementTransform(a, target))
什么时候用哪个
| 你想做的事 | 写法 |
|---|---|
| 让一个物体压在所有东西上面 | mob.set_z_index(10),比别人都大就行 |
| 让底框、背景网格垫到最底下 | mob.set_z_index(-1) |
都没设 z_index,把其中一个提到最上面 |
self.bring_to_front(mob) 或再 self.add(mob) 一次 |
都没设 z_index,把其中一个压到最下面 |
self.bring_to_back(mob) |
| 整个组一起换层 | group.set_z_index(1),成员都会被设上 |
| 复制出来的物体 | mob.copy() 会带着原来的 z_index |
| 查现在的绘制顺序 | self.get_mobject_family_members() |
常见写错的地方
用 bring_to_front 去压一个 z_index 更高的物体
blue.set_z_index(1)
self.add(red, blue)
self.bring_to_front(red) # 红还是在蓝下面
bring_to_front 在 0.21.0 里的函数体只有一句 self.add(*mobjects),也就是把物体重新加一次,让它排到添加顺序的最后。它改的是添加顺序,而添加顺序只在 z_index 相同的时候才起作用。蓝是 1,红是 0,红加多少次都在蓝下面。
两个物体都没设 z_index 的时候,bring_to_front(red) 是管用的,我们试过,红到了上面。bring_to_back 同理。add_foreground_mobject 也压不过:红用它加成前景物体,蓝的 z_index 是 1,重叠处还是蓝色。

场景里只要有人用了 z_index,后面调层次就都用 z_index,别两套混着来。
把 z_index 写在 VGroup 的括号里
card = VGroup(plate, label, z_index=5)
这样写不报错,也没有效果。构造函数里的 z_index=5 只设给了组自己,plate 和 label 还是 0。组本身没有可画的东西,它排在哪都不影响画面。我们拿这样一个组和一个后加的蓝方块比,蓝方块照样盖在上面;打印绘制顺序,两个方块的 z_index 都是 0,只有 VGroup 那一项是 5。
改成 VGroup(plate, label).set_z_index(5)。set_z_index(5, family=False) 和写在括号里是一个效果,同样不起作用。
想用 shift(OUT) 把物体抬到前面
red.shift(OUT * 5)
普通的 Scene 是从正面平着看的,物体的 z 坐标不参与遮挡。我们把红方块往外挪了 5 个单位,后加的蓝方块还是盖在它上面。
如果手里的物体本来就带着 z 坐标,想按它来定层次,可以调 red.set_z_index_by_z_Point3D(),它会把物体中心的 z 坐标抄给 z_index。上面那个红方块调完之后 z_index 是 5.0,就到蓝方块上面了。
常见追问(FAQ)
Q:z_index 的默认值是多少?能用负数和小数吗?
A:默认是 0。负数和小数都能用,我们试过 set_z_index(-1) 和 set_z_index(0.5),都按大小正常排。想在 0 和 1 之间再插一层,不用把别的数字全改一遍,给 0.5 就行。
Q:物体已经 add 进场景了,再调 set_z_index 还管用吗?
A:管用。Manim 每画一帧都重新排一次,改完之后的下一帧就按新的数字来。本文第一段代码就是先 add 后设的。
Q:always_redraw 生成的物体,z_index 写在哪?
A:写在 lambda 里面或者外面都行。我们两种都试了:always_redraw(lambda: Square().set_z_index(1)),以及先 always_redraw(...) 再对返回的物体调 set_z_index(1),播了几帧之后 z_index 都还是 1,遮挡关系也对。
Q:a.become(b) 会把 b 的 z_index 带过来吗?
A:不会。我们让一个默认的方块 become 一个 z_index = 4 的方块,之后它的 z_index 还是 0。copy() 是带的,复制出来的物体 z_index 也是 4。
Q:3D 场景里 z_index 还管用吗?
A:平面图形之间管用。我们在 ThreeDScene 里从正上方看两个重叠的方块,红的 z_index = 1,蓝的后加,红在上面。球面、曲面这类带明暗的 3D 物体,Manim 的 3D 相机还会按它们离镜头的远近再排一次,那是另一套规则,这篇不展开。
Q:能不能让 Manim 完全不看 z_index?
A:能,在 construct 开头写 self.camera.use_z_index = False,之后只按添加顺序画。我们这样设了之后,z_index = 1 的红方块被后加的蓝方块盖住了。一般用不到,接手别人的场景、想先排除 z_index 的影响时可以临时开一下。
z_index 和添加顺序在教程《布局(下)· 尺寸 / 包围 / 编组 / 深度 + 实战》的第四节「深度方向布局 z_index」里有视频讲解,旁边的代码可以边看边改。同一课第三节讲 VGroup,文字版是《Manim 怎么把一组物体排成一行或网格?》。
来源:极坐标⋅XYZ · jizuobiao.xyz · 更新于 2026-10-06 · 基于 Manim Community 0.21.0