跳转至

3D 场景

影片代码
import manimgx as m
import numpy as np


class ThreeDScenesHero(m.ThreeDScene):
    def construct(self) -> None:
        axes = m.ThreeDAxes(
            x_range=[-3, 3],
            y_range=[-3, 3],
            z_range=[0, 2],
            x_length=7,
            y_length=7,
            z_length=3,
        )

        def height(u: float, v: float) -> np.ndarray:
            return axes.c2p(u, v, 2 * np.exp(-(u**2 + v**2) / 2))

        hill = m.Surface(height, u_range=[-2.5, 2.5], v_range=[-2.5, 2.5])
        title = m.Text("A hill", font_size=40).to_corner(m.UL)
        self.add_fixed_in_frame_mobjects(title)
        self.add(axes, hill)
        self.move_camera(phi=65 * m.DEGREES, theta=-45 * m.DEGREES, run_time=2)
        self.begin_ambient_camera_rotation(rate=0.3)
        self.wait(3)

3D 场景是相机从某个角度观察画框的场景。两个角度决定它:phi,自正上方度量(0 为正下方 俯视,如同平面场景),以及 theta,绕垂直轴度量。相机始终对准画框中心;它以透视方式显示 场景,近处物体遮挡远处物体。

ThreeDScene

ThreeDSceneExample
代码
import numpy as np

import manimgx as m


class ThreeDSceneExample(m.ThreeDScene):
    def construct(self) -> None:
        self.set_camera_orientation(
            phi=65 * m.DEGREES, theta=-50 * m.DEGREES, zoom=0.6
        )
        axes = m.ThreeDAxes(x_range=(-3, 3), y_range=(-3, 3), z_range=(-2, 2))
        surface = m.Surface(
            lambda u, v: axes.c2p(u, v, np.sin(u) * np.cos(v)),
            u_range=(-3, 3),
            v_range=(-3, 3),
            resolution=(24, 24),
        ).set_fill_by_checkerboard(m.BLUE_D, m.TEAL, opacity=0.9)
        title = m.MathTex(r"z = \sin x \cos y", font_size=64).to_corner(m.UL)
        self.add_fixed_in_frame_mobjects(title)
        self.add(axes, surface)

以三维方式观察的场景:相机绕其轨道运动,带透视与深度。

相机从角度 phi(自 z 轴起,0 表示从正上方俯视 xy 平面)与 theta(绕 z 轴,默认 −90°,使 x 指向右方)观察其帧的中心;gamma 使其绕视线转动, zoom 放大,focal_distance 为其与所观察点的距离。每个都是 相机 的值跟踪器,因此像任何 mobject 一样移动:move_camera 为其添加 动画,环境旋转使其持续转动。较近的表面遮挡较远者;固定在帧中的 mobject 平铺绘制在屏幕上,位于一切之上。

class MyScene(m.ThreeDScene):
源码

manimgx\scene.py

def __init__(self) -> None:
    clock.reset()  # a new world: its clock at 0
    self.camera = Camera(three_d=self.three_d)
    """场景的相机:一帧所显示的内容。移动、缩放或为其
    [`frame`][manimgx.Camera.frame] 添加动画以平移与缩放。"""
    self.mobjects: list[Mobject] = []
    """场景持有的 mobject,按绘制顺序排列:每个绘制在先于它的那些之上,
    z-index 相等时如此。[`add`][manimgx.Scene.add] 与
    [`remove`][manimgx.Scene.remove] 改变它。"""
    self.foreground_mobjects: list[Mobject] = []
    """保持在最前的 mobject:绘制在所有其他 mobject 之上,z-index 相等时
    如此(见 [`add_foreground_mobjects`][manimgx.Scene.add_foreground_mobjects])。
    它们也在场景的 [`mobjects`][manimgx.Scene.mobjects] 之中。"""
    self.updaters: list[Callable[[float], object]] = []
    """场景自身的更新器,按其运行顺序排列(见
    [`add_updater`][manimgx.Scene.add_updater])。"""
    self._since: dict[object, Fraction] = {}  # when each scene updater was last run
    self.clock = Fraction(0)
    """场景的时间,精确的:秒的分数(见
    [`time`][manimgx.Scene.time])。"""
    self.frame = 0
    """场景已记录的帧数:下一帧的编号,该帧显示 `frame / fps` 秒处的世界。"""
    self._records = False  # does anything record the world (`_recording`), this run
    # the play's instants no frame shows, with what each concerns (`_exact`, `_ticks`)
    self._events: dict[Fraction, Concern | None] = {}
    self._playing: list[Mobject] = []  # what the play running acts on (`_running`)
    # while animations begin together (`_together`): what the scene held, by id, and
    # the list of mobjects it was found in (`_introduce`)
    self._beginning = False
    self._holding: tuple[list[Mobject], set[int]] | None = None
    # the play running: its animations may hide parts of the scene (`display_list`)
    self._animation: Animation | None = None
    self.num_plays = 0
    """场景已运行的 play 与 wait 次数。"""
    self.film = Film()
    """场景记录的影片:[`render`][manimgx.Scene.render] 新建一个,
    并返回它。"""

set_camera_orientation

立即设置相机观察位置:每个给定设置;其余保持不变。

self.set_camera_orientation(phi=None, theta=None, gamma=None, zoom=None, focal_distance=None, frame_center=None)
phi

相机视线与 z 轴之间的夹角,以弧度表示:0 表示从正上方俯视 xy 平面。

theta

相机绕 z 轴的角度,以弧度表示,从 x 轴逆时针方向计量:在 −90° 时从 y 轴负侧观察,使 x 轴指向右方。

gamma

相机绕自身视线转动的角度,以弧度表示。

zoom

相机的放大倍数:2 表示将一切放大两倍显示。

focal_distance

相机与所观察点的距离,以场景单位表示:越近, 透视越强。

frame_center

相机所观察的点,或其中心为该点的 mobject:相机的帧 移动到该处。

源码

manimgx\scene.py

def set_camera_orientation(
    self,
    phi: float | None = None,
    theta: float | None = None,
    gamma: float | None = None,
    zoom: float | None = None,
    focal_distance: float | None = None,
    frame_center: Point3DLike | Mobject | None = None,
) -> None:
    """立即设置相机观察位置:每个给定设置;其余保持不变。

    Args:
        phi: 相机视线与 z 轴之间的夹角,以弧度表示:0 表示从正上方俯视
            xy 平面。
        theta: 相机绕 z 轴的角度,以弧度表示,从 x 轴逆时针方向计量:在
            −90° 时从 y 轴负侧观察,使 x 轴指向右方。
        gamma: 相机绕自身视线转动的角度,以弧度表示。
        zoom: 相机的放大倍数:2 表示将一切放大两倍显示。
        focal_distance: 相机与所观察点的距离,以场景单位表示:越近,
            透视越强。
        frame_center: 相机所观察的点,或其中心为该点的 mobject:相机的帧
            移动到该处。
    """
    cam = self.camera
    for value, tracker in (
        (phi, cam.phi_tracker),
        (theta, cam.theta_tracker),
        (gamma, cam.gamma_tracker),
        (zoom, cam.zoom_tracker),
        (focal_distance, cam.focal_distance_tracker),
    ):
        if value is not None:
            tracker.set_value(value)
    if frame_center is not None:
        cam.frame.move_to(frame_center)

begin_ambient_camera_rotation

代码
import manimgx as m


class ThreeDSceneAmbientRotationExample(m.ThreeDScene):
    def construct(self) -> None:
        self.set_camera_orientation(
            phi=70 * m.DEGREES, theta=-45 * m.DEGREES
        )
        torus = m.Torus(major_radius=2.5, minor_radius=0.8)
        self.add(m.ThreeDAxes(), torus.set_color(m.TEAL))
        self.begin_ambient_camera_rotation(rate=m.PI / 4)  # 45° a second
        self.wait(4)

开始稳定转动相机,贯穿 play 与 wait,直到 stop_ambient_camera_rotation。

相机的一个角度以稳定速率增长:其跟踪器获得一个更新器 (一个 flow)并加入场景。

self.begin_ambient_camera_rotation(rate=0.02, about='theta')
rate

角度增长速率,以弧度每秒表示;负值反方向转动。

about

该角度:"theta"(绕 z 轴)、"phi" 或 "gamma",任意大小写。

源码

manimgx\scene.py

def begin_ambient_camera_rotation(
    self, rate: float = 0.02, about: str = "theta"
) -> None:
    """开始稳定转动相机,贯穿 play 与 wait,直到
    `stop_ambient_camera_rotation`。

    相机的一个角度以稳定速率增长:其跟踪器获得一个更新器
    (一个 [flow][manimgx.mobject.flow])并加入场景。

    Args:
        rate: 角度增长速率,以弧度每秒表示;负值反方向转动。
        about: 该角度:"theta"(绕 z 轴)、"phi" 或 "gamma",任意大小写。

    Examples:
        ```python
        import manimgx as m


        class ThreeDSceneAmbientRotationExample(m.ThreeDScene):
            def construct(self) -> None:
                self.set_camera_orientation(
                    phi=70 * m.DEGREES, theta=-45 * m.DEGREES
                )
                torus = m.Torus(major_radius=2.5, minor_radius=0.8)
                self.add(m.ThreeDAxes(), torus.set_color(m.TEAL))
                self.begin_ambient_camera_rotation(rate=m.PI / 4)  # 45° a second
                self.wait(4)
        ```
    """
    tracker = self._tracker(about)
    tracker.add_updater(flow(lambda m, dt: m.increment_value(rate * dt)))
    self.add(tracker)

stop_ambient_camera_rotation

停止转动相机:移除该角度跟踪器的每个更新器,跟踪器离开场景。相机 停留在其转到的位置。

self.stop_ambient_camera_rotation(about='theta')
about

该角度:"theta"、"phi" 或 "gamma",任意大小写。

源码

manimgx\scene.py

def stop_ambient_camera_rotation(self, about: str = "theta") -> None:
    """停止转动相机:移除该角度跟踪器的每个更新器,跟踪器离开场景。相机
    停留在其转到的位置。

    Args:
        about: 该角度:"theta"、"phi" 或 "gamma",任意大小写。
    """
    tracker = self._tracker(about)
    tracker.clear_updaters()
    self.remove(tracker)

begin_3dillusion_camera_rotation

开始使相机绕其朝向摇摆,以产生深度错觉,贯穿 play 与 wait,直到 stop_3dillusion_camera_rotation。

相机小幅环绕:theta 在 origin_theta 两侧各 0.2 弧度间摆动, phi 在 origin_phi 与少 0.2 弧度之间,二者相差四分之一周期。两个 角度的跟踪器获得更新器(flow)并加入场景。

self.begin_3dillusion_camera_rotation(rate=1, origin_phi=None, origin_theta=None)
rate

摇摆速度,以每秒其周期的弧度表示:一个周期耗时 2π / rate 秒。

origin_phi

摇摆起自的 phi;None 表示相机当前值。

origin_theta

摇摆所绕的 theta;None 表示相机当前值。

源码

manimgx\scene.py

def begin_3dillusion_camera_rotation(
    self,
    rate: float = 1,
    origin_phi: float | None = None,
    origin_theta: float | None = None,
) -> None:
    """开始使相机绕其朝向摇摆,以产生深度错觉,贯穿 play 与 wait,直到
    `stop_3dillusion_camera_rotation`。

    相机小幅环绕:`theta` 在 `origin_theta` 两侧各 0.2 弧度间摆动,
    `phi` 在 `origin_phi` 与少 0.2 弧度之间,二者相差四分之一周期。两个
    角度的跟踪器获得更新器(flow)并加入场景。

    Args:
        rate: 摇摆速度,以每秒其周期的弧度表示:一个周期耗时
            2π / `rate` 秒。
        origin_phi: 摇摆起自的 `phi`;None 表示相机当前值。
        origin_theta: 摇摆所绕的 `theta`;None 表示相机当前值。
    """
    cam = self.camera
    origin_theta = cam.get_theta() if origin_theta is None else origin_theta
    origin_phi = cam.get_phi() if origin_phi is None else origin_phi
    clocks = [0.0, 0.0]

    def swing_theta(m: ValueTracker, dt: float) -> None:
        clocks[0] += dt * rate
        m.set_value(origin_theta + 0.2 * np.sin(clocks[0]))

    def swing_phi(m: ValueTracker, dt: float) -> None:
        clocks[1] += dt * rate
        m.set_value(origin_phi + 0.1 * np.cos(clocks[1]) - 0.1)

    cam.theta_tracker.add_updater(flow(swing_theta))
    cam.phi_tracker.add_updater(flow(swing_phi))
    self.add(cam.theta_tracker, cam.phi_tracker)

stop_3dillusion_camera_rotation

停止摇摆相机:移除 theta 与 phi 跟踪器的每个更新器,它们离开 场景。相机停留在摇摆所留位置。

self.stop_3dillusion_camera_rotation()
源码

manimgx\scene.py

def stop_3dillusion_camera_rotation(self) -> None:
    """停止摇摆相机:移除 `theta` 与 `phi` 跟踪器的每个更新器,它们离开
    场景。相机停留在摇摆所留位置。"""
    for tracker in (self.camera.theta_tracker, self.camera.phi_tracker):
        tracker.clear_updaters()
        self.remove(tracker)

move_camera

代码
import manimgx as m


class ThreeDSceneMoveCameraExample(m.ThreeDScene):
    def construct(self) -> None:
        cube = m.Cube(side_length=2.5, fill_opacity=0.8, fill_color=m.BLUE)
        self.add(m.ThreeDAxes(), cube)
        self.move_camera(
            phi=70 * m.DEGREES, theta=-45 * m.DEGREES, run_time=2
        )
        self.move_camera(theta=45 * m.DEGREES, zoom=1.5, run_time=2)

在一次 play 中为相机添加动画到新朝向:每个给定设置;其余保持不变。

每个设置从其值稳定移动到新值,由速率函数缓动,因此大 2π 的 theta 使相机绕一周。帧在给定 frame_center 时移动到该处,added_anims 一同 播放。

self.move_camera(phi=None, theta=None, gamma=None, zoom=None, focal_distance=None, frame_center=None, added_anims=(), **kwargs)
phi

相机视线与 z 轴之间的夹角,以弧度表示:0 表示从正上方俯视 xy 平面。

theta

相机绕 z 轴的角度,以弧度表示,从 x 轴逆时针方向计量。

gamma

相机绕自身视线转动的角度,以弧度表示。

zoom

相机的放大倍数:2 表示将一切放大两倍显示。

focal_distance

相机与所观察点的距离,以场景单位表示。

frame_center

相机要观察的点,或其中心为该点的 mobject。

added_anims

一同播放的动画。

run_time

动画播放时长,以秒为单位(默认 1)。

lag_ratio

mobject 各部分如何交错:每个部分在其前一个部分开始后,经过其运行时长的该分数比例后开始 (默认 0:全部同时;1:一个接一个)。

rate_func

动画进度如何随时间运行:一个从 [0, 1] 到 [0, 1] 的函数 (默认为 smooth;参见 rate functions)。

reverse_rate_function

是否反向运行动画(默认 False)。

name

动画的名称。

remover

动画结束时 mobject 是否离开场景 (默认 False)。

suspend_mobject_updating

动画期间 mobject 的更新器是否在其下运行(默认 True):它们 持续作用于 mobject,每一帧显示动画应用于其结果。 若为 False,则它们作用于被动画的 mobject 本身。

introducer

动画开始时 mobject 是否加入场景(默认 False); 否则若场景缺少它,play 在其开始时将其带入。

use_override

若某 mobject 的类用另一个动画代替此动画播放,是否如此执行 (默认 True)。

还接受 变换关键字。

源码

manimgx\scene.py

def move_camera(
    self,
    phi: float | None = None,
    theta: float | None = None,
    gamma: float | None = None,
    zoom: float | None = None,
    focal_distance: float | None = None,
    frame_center: Point3DLike | Mobject | None = None,
    added_anims: Sequence[Animation] = (),
    **kwargs: Unpack[TransformOptions],
) -> None:
    """在一次 play 中为相机添加动画到新朝向:每个给定设置;其余保持不变。

    每个设置从其值稳定移动到新值,由速率函数缓动,因此大 2π 的 `theta`
    使相机绕一周。帧在给定 `frame_center` 时移动到该处,`added_anims` 一同
    播放。

    Args:
        phi: 相机视线与 z 轴之间的夹角,以弧度表示:0 表示从正上方俯视
            xy 平面。
        theta: 相机绕 z 轴的角度,以弧度表示,从 x 轴逆时针方向计量。
        gamma: 相机绕自身视线转动的角度,以弧度表示。
        zoom: 相机的放大倍数:2 表示将一切放大两倍显示。
        focal_distance: 相机与所观察点的距离,以场景单位表示。
        frame_center: 相机要观察的点,或其中心为该点的 mobject。
        added_anims: 一同播放的动画。
        **kwargs: play 中每个动画的[变换选项][manimgx.animation.transform.TransformOptions],
            附加动画亦然。

    Examples:
        ```python
        import manimgx as m


        class ThreeDSceneMoveCameraExample(m.ThreeDScene):
            def construct(self) -> None:
                cube = m.Cube(side_length=2.5, fill_opacity=0.8, fill_color=m.BLUE)
                self.add(m.ThreeDAxes(), cube)
                self.move_camera(
                    phi=70 * m.DEGREES, theta=-45 * m.DEGREES, run_time=2
                )
                self.move_camera(theta=45 * m.DEGREES, zoom=1.5, run_time=2)
        ```
    """
    cam = self.camera
    pairs = (
        (phi, cam.phi_tracker),
        (theta, cam.theta_tracker),
        (focal_distance, cam.focal_distance_tracker),
        (gamma, cam.gamma_tracker),
        (zoom, cam.zoom_tracker),
    )
    anims: list[Animation] = [
        tracker.animate.set_value(value)
        for value, tracker in pairs
        if value is not None
    ]
    if frame_center is not None:
        anims.append(cam.frame.animate.move_to(frame_center))
    self.play(*anims, *added_anims, **kwargs)
    if frame_center is not None:
        self.remove(cam.frame)

add_fixed_in_frame_mobjects

添加固定到屏幕的 mobject:每个按其点所在位置绘制,如同在以原点为中心的 二维视图中,绘制在一切之上。

它们被添加到场景,无论相机的角度、缩放与中心如何都保持在屏幕上的位置: 用 to_corner 放置的标题停留在其角落。其中的一切现在及以后都被固定: 之后添加的成员(一个数字的新数位、更新器或 Transform 产生的部分)也 被固定。

self.add_fixed_in_frame_mobjects(*mobjects)
源码

manimgx\scene.py

def add_fixed_in_frame_mobjects(self, *mobjects: Mobject) -> None:
    """添加固定到屏幕的 mobject:每个按其点所在位置绘制,如同在以原点为中心的
    二维视图中,绘制在一切之上。

    它们被添加到场景,无论相机的角度、缩放与中心如何都保持在屏幕上的位置:
    用 `to_corner` 放置的标题停留在其角落。其中的一切现在及以后都被固定:
    之后添加的成员(一个数字的新数位、更新器或 Transform 产生的部分)也
    被固定。
    """
    self.add(*mobjects)
    self.camera.add_fixed_in_frame_mobjects(*mobjects)