Manim 3D 场景:ThreeDScene、相机控制与 3D 曲面动画
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) 肉眼几乎无差、时间减半。