首页 / 文档中心 / 进阶

Manim 3D 场景:ThreeDScene、相机控制与 3D 曲面动画

发布于 2026-11-02 · 进阶系列 · 约 4 分钟读完 · 中文正文 + English TL;DR
▶ 渲染效果实录(manim 渲染输出,代码见正文,可直接复现)
English TL;DR

3D in Manim CE is mostly about the camera. ThreeDScene gives you spherical camera coordinates: phi (polar angle from the z-axis) and theta (azimuthal angle in the xy-plane), set with set_camera_orientation and animated with move_camera. begin_ambient_camera_rotation() spins theta continuously — the signature look of 3D showcases. ThreeDAxes provides c2p() to lift math coordinates into 3D screen space, and Surface(lambda u, v: ...) parametrizes meshes; checkerboard_colors plus fill_opacity give the classic shaded look, and SurfaceMesh adds wireframe. Text must be handled specially: either add_fixed_frame_mobjects() to pin labels to the screen, or accept billboard rotation. The chapter walks through a full scene — axes, sine-wave surface, ambient rotation, then an animated fly-in of the camera — and closes with common pitfalls: forgetting to orient the camera before adding objects, text that spins illegibly, and z-fighting artifacts from coincident surfaces.

3D 的核心:先摆相机,再放东西

Manim 的 3D 不是"建模器"而是"相机语言"。相机位置用两个球坐标角描述:

  • phi — 极角:相机从正上方压下来的角度(0° 正上方俯视,90° 平视)
  • theta — 方位角:相机绕 z 轴转到哪个方向
  • zoom — 焦距缩放
from manim import *

class Surface3D(ThreeDScene):
    def construct(self):
        self.set_camera_orientation(phi=70 * DEGREES, theta=-45 * DEGREES)

        axes = ThreeDAxes(x_range=[-3, 3], y_range=[-3, 3], z_range=[-2, 2])
        surf = Surface(
            lambda u, v: axes.c2p(u, v, np.sin(u) * np.cos(v)),
            u_range=[-3, 3], v_range=[-3, 3],
            resolution=(28, 28),
            fill_opacity=0.7,
            checkerboard_colors=[BLUE_D, BLUE_E],
        )
        self.add(axes)
        self.play(Create(surf), run_time=3)
        self.begin_ambient_camera_rotation(rate=0.15)
        self.wait(3)

运镜:move_camera

# 环绕 + 逼近的组合运镜
self.move_camera(theta=-30 * DEGREES, phi=60 * DEGREES, zoom=1.4,
                 run_time=3)
self.stop_ambient_camera_rotation()
self.move_camera(phi=75 * DEGREES, theta=135 * DEGREES, run_time=2)

运镜本身就是叙事:环绕展示立体感、俯视讲截面、平视讲高度。3b1b 的 3D 章节几乎全靠 phi/theta 的组合讲故事。

参数化曲面:Surface

Surface 接收 u→v 的参数化函数,返回三维网格:

# 球面
sphere = Sphere(radius=2, resolution=(24, 24),
                fill_opacity=0.5, checkerboard_colors=[TEAL_D, TEAL_E])

# 圆环
torus = Torus(r1=2, r2=0.6)

# 波纹面: 振幅随半径衰减
ripple = Surface(
    lambda u, v: np.array([
        u * np.cos(v),
        u * np.sin(v),
        0.4 * np.sin(3 * u) * np.exp(-0.3 * u)]),
    u_range=[0.001, 4], v_range=[0, 2 * np.pi],
    fill_opacity=0.8, checkerboard_colors=[PURPLE_D, PURPLE_E])

3D 里的文字:两个选择

3D 场景里的 Text 会跟随场景旋转(经常转到看不清)。两种处理:

title = Text("曲面: z = sin(x)cos(y)", font="Microsoft YaHei",
             font_size=32).to_edge(UP)
self.add_fixed_frame_mobjects(title)   # 钉在屏幕上, 不随相机转

或者反过来——让文字成为场景一员(比如贴在曲面旁的 3D 标签),就要接受它随视角变化。教学片头标题用 add_fixed_frame_mobjects,空间标注用 3D 文字。

MovingCameraScene:2D 世界的运镜

顺带讲清楚另一个"相机":MovingCameraScene 是 2D 平面的取景框动画,与 3D 相机无关:

class Pan(MovingCameraScene):
    def construct(self):
        s = Square().shift(LEFT * 6)
        c = Circle().shift(RIGHT * 6)
        self.add(s, c)
        self.camera.frame.save_state()
        self.play(self.camera.frame.animate.move_to(s).set(width=4))
        self.play(self.camera.frame.animate.move_to(c).set(width=8))

常见坑

  • 忘了先 set_camera_orientation:默认平视视角下 z 轴正对屏幕,曲面看起来是一条线。
  • 文字乱转:见上文 add_fixed_frame_mobjects。
  • 曲面黑块:fill_opacity 设 0.6~0.8 半透明可缓解遮挡混乱;两个曲面完全重合会闪烁(z-fighting),错开一点即可。
  • 渲染慢:resolution 从 (32,32) 降到 (20,20) 肉眼几乎无差、时间减半。