导航

利益相关:本文作者与极坐标⋅XYZ(jizuobiao.xyz)团队有关联。下面的结论是我们在 Manim Community 0.21.0(2026 年的最新版本)上渲出画面、读像素颜色核对的,代码都贴在文里,结论请自行验证。

self.bring_to_front(mob) 调了之后物体还是被盖着,基本都是同一个原因:盖住它的那个东西 z_index 比它高。Manim 画一帧的时候先按 z_index 排,数字相同的再按添加顺序排。bring_to_front 改的只是添加顺序,轮不到它说话。改法是给要置顶的物体一个更大的 z_index,比如 mob.set_z_index(2)。

Manim 是用 Python 代码做数学动画的开源引擎,3Blue1Brown 的视频就是它做的。它没有图层面板,重叠的物体谁在上面,靠的是两样东西:物体加进场景的先后,和每个物体身上一个叫 z_index 的数字,默认是 0。

先复现

两个重叠的方块,蓝的设了 z_index = 1,然后想把红的提到上面:

from manim import *


class BringToFrontFails(Scene):
    def construct(self):
        red = Square(side_length=2, color=RED).set_fill(RED, 1).move_to([-0.5, 0, 0])
        blue = Square(side_length=2, color=BLUE).set_fill(BLUE, 1).move_to([0.5, 0, 0])
        blue.set_z_index(1)

        self.add(red, blue)
        self.bring_to_front(red)
        self.wait()

渲出来,两个方块重叠的地方是蓝色。我们把 bring_to_front(red) 连着调了三次,还是蓝色。

把 blue.set_z_index(1) 那一行删掉再跑,重叠处就是红色了,bring_to_front 正常工作。

Manim 的 bring_to_front 对比:两个方块 z_index 都是 0 时,bring_to_front(red) 之后红在上面;blue 设过 z_index = 1 时,bring_to_front(red) 之后红还是被蓝盖着,要改成 red.set_z_index(2)

bring_to_front 到底做了什么

0.21.0 的源码里,它的函数体只有两行:

def bring_to_front(self, *mobjects):
    self.add(*mobjects)
    return self

self.add 对一个已经在场景里的物体,会先把它从列表里拿掉,再接到末尾。所以上面那段代码调完之后,self.mobjects 是 [blue, red],红确实排到了最后。

可是画的时候,Manim 还要对这张表按 z_index 排一次序。我们把排完的结果打印出来:

print([(m.color, m.z_index) for m in self.get_mobject_family_members()])
# 红 0,蓝 1

红是 0,蓝是 1,蓝排在后面,后画,盖住红。不管红在 self.mobjects 里排第几,这一步都会把它排回蓝的前面。

bring_to_back 是同样的道理,它把物体挪到列表开头,同样压不住一个 z_index 更低的东西。add_foreground_mobject 我们也试了:红用它加成前景物体,蓝的 z_index 是 1,重叠处还是蓝色。

自己没设过 z_index,也会碰到

有些 Manim 自带的对象,内部已经设了 z_index。Graph 是一个:它的顶点是 0,边是 -1,这样边永远在顶点下面。

副作用是,场景里任何一个 z_index 为 0 的实心物体,只要位置和边重叠,都会把边盖住,和谁先加进场景无关:

plate = Rectangle(width=6, height=3, color=BLUE).set_fill(BLUE, 1)
g = Graph([1, 2], [(1, 2)], layout={1: [-2, 0, 0], 2: [2, 0, 0]})
self.add(plate, g)          # 底板先加,图后加
self.bring_to_front(g)      # 再提一次

渲出来两个顶点看得见,中间那条边看不见,被底板挡住了。底板是先加的,图是后加的,还 bring_to_front 了一次,都没用,因为边是 -1,底板是 0。

把底板降到 -2,边就出来了:

plate.set_z_index(-2)

碰到「明明后加的,却被先加的盖住」,先看一眼被盖的东西是不是哪个库对象的一部分。

三种改法

给要置顶的物体一个更大的数

知道对手是几,就比它大一点。不知道的话,问场景要当前的最大值:

top = max(m.z_index for m in self.get_mobject_family_members())
red.set_z_index(top + 1)

我们在一个 z_index 最大是 7 的场景里这样写,红被设成 8,到了最上面。

把挡路的那个降下去

像上面底板的例子,该垫底的东西给负数,比把别的东西一个个抬高省事。

全部清零,回到只看添加顺序

接手一个到处是 z_index 的场景,想先弄清楚画面时可以这样:

for m in self.get_mobject_family_members():
    m.z_index = 0

清完之后 bring_to_front(red) 就管用了,我们试过,重叠处变回红色。还有一个开关 self.camera.use_z_index = False,效果一样,区别是它不改物体身上的数字。

平时写场景,我们的习惯是二选一:要么全程不碰 z_index,只靠添加顺序和 bring_to_front;要么只要用了一处 z_index,后面调层次就都用它。

顺带一个副作用:别对组里的成员 bring_to_front

bring_to_front 是重新 add,这在物体属于某个组的时候会改掉场景的结构。

g = VGroup(red, blue, green)
self.add(g)
print(self.mobjects)        # [VGroup]

self.bring_to_front(red)
print(self.mobjects)        # [Square(蓝), Square(绿), Square(红)]

调之前,场景列表里只有 g 一项。调之后,g 从列表里消失了,换成了三个散着的方块。g 这个对象还在,里面还是三个成员,但场景已经不把它当成一个整体记着。

接下来 self.remove(g) 就不灵了。我们试了,调完之后三个方块还在画面上,self.mobjects 一个没少。remove 找的是列表里的 g,而列表里已经没有它。

要把组里的一个成员提到上面,用 red.set_z_index(1),它不动场景列表。

小结

情况 bring_to_front(mob) mob.set_z_index(n)
场景里都没设过 z_index 管用 管用
挡住它的物体 z_index 更高 没用 设成更大的数就行
被挡的是 Graph 的边这类自带负 z_index 的东西 没用 把挡路的降到更低
物体是某个组的成员 会把组在场景列表里拆散 不影响场景列表
之后又有新物体 add 或 FadeIn 进来 新来的又到它上面 不受影响

最后一行我们也试了:bring_to_front(red) 之后 self.play(FadeIn(green)),绿在红上面。bring_to_front 只是那一刻排到最后,后面进场的照样排在它后头。

z_index 的完整规则,包括组的 z_index 怎么传给成员、ReplacementTransform 播完为什么会跳层、3D 场景里还管不管用,在极坐标⋅XYZ 的这篇文章里写得更细;配套视频在教程《布局(下)· 尺寸 / 包围 / 编组 / 深度 + 实战》的第四节。

实测环境:Manim Community 0.21.0 · 2026 年 10 月