mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00

* Comment tweak * Directly print traceback Since the shell.showtraceback is giving some issues * Make InteracrtiveSceneEmbed into a class This way it can keep track of it's internal shell; use of get_ipython has a finicky relationship with reloading. * Move remaining checkpoint_paste logic into scene_embed.py This involved making a few context managers for Scene: temp_record, temp_skip, temp_progress_bar, which seem useful in and of themselves. * Change null key to be the empty string * Ensure temporary svg paths for Text are deleted * Remove unused dict_ops.py functions * Remove break_into_partial_movies from file_writer configuration * Rewrite guarantee_existence using Path * Clean up SceneFileWriter It had a number of vestigial functions no longer used, and some setup that could be made more organized. * Remove --save_pngs CLI arg (which did nothing) * Add --subdivide CLI arg * Remove add_extension_if_not_present * Remove get_sorted_integer_files * Have find_file return Path * Minor clean up * Clean up num_tex_symbols * Fix find_file * Minor cleanup for extract_scene.py * Add preview_frame_while_skipping option to scene config * Use shell.showtraceback function * Move keybindings to config, instead of in-place constants * Replace DEGREES -> DEG
266 lines
8.6 KiB
Python
266 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
import warnings
|
|
|
|
import numpy as np
|
|
from scipy.spatial.transform import Rotation
|
|
|
|
from manimlib.constants import DEG, RADIANS
|
|
from manimlib.constants import FRAME_SHAPE
|
|
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
|
from manimlib.constants import PI
|
|
from manimlib.mobject.mobject import Mobject
|
|
from manimlib.utils.space_ops import normalize
|
|
from manimlib.utils.simple_functions import clip
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from manimlib.typing import Vect3
|
|
|
|
|
|
class CameraFrame(Mobject):
|
|
def __init__(
|
|
self,
|
|
frame_shape: tuple[float, float] = FRAME_SHAPE,
|
|
center_point: Vect3 = ORIGIN,
|
|
# Field of view in the y direction
|
|
fovy: float = 45 * DEG,
|
|
euler_axes: str = "zxz",
|
|
# This keeps it ordered first in a scene
|
|
z_index=-1,
|
|
**kwargs,
|
|
):
|
|
super().__init__(z_index=z_index, **kwargs)
|
|
|
|
self.uniforms["orientation"] = Rotation.identity().as_quat()
|
|
self.uniforms["fovy"] = fovy
|
|
|
|
self.default_orientation = Rotation.identity()
|
|
self.view_matrix = np.identity(4)
|
|
self.id4x4 = np.identity(4)
|
|
self.camera_location = OUT # This will be updated by set_points
|
|
self.euler_axes = euler_axes
|
|
|
|
self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
|
|
self.set_width(frame_shape[0], stretch=True)
|
|
self.set_height(frame_shape[1], stretch=True)
|
|
self.move_to(center_point)
|
|
|
|
def set_orientation(self, rotation: Rotation):
|
|
self.uniforms["orientation"][:] = rotation.as_quat()
|
|
return self
|
|
|
|
def get_orientation(self):
|
|
return Rotation.from_quat(self.uniforms["orientation"])
|
|
|
|
def make_orientation_default(self):
|
|
self.default_orientation = self.get_orientation()
|
|
return self
|
|
|
|
def to_default_state(self):
|
|
self.set_shape(*FRAME_SHAPE)
|
|
self.center()
|
|
self.set_orientation(self.default_orientation)
|
|
return self
|
|
|
|
def get_euler_angles(self) -> np.ndarray:
|
|
orientation = self.get_orientation()
|
|
if np.isclose(orientation.as_quat(), [0, 0, 0, 1]).all():
|
|
return np.zeros(3)
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter('ignore', UserWarning) # Ignore UserWarnings
|
|
angles = orientation.as_euler(self.euler_axes)[::-1]
|
|
# Handle Gimble lock case
|
|
if self.euler_axes == "zxz":
|
|
if np.isclose(angles[1], 0, atol=1e-2):
|
|
angles[0] = angles[0] + angles[2]
|
|
angles[2] = 0
|
|
if np.isclose(angles[1], PI, atol=1e-2):
|
|
angles[0] = angles[0] - angles[2]
|
|
angles[2] = 0
|
|
return angles
|
|
|
|
def get_theta(self):
|
|
return self.get_euler_angles()[0]
|
|
|
|
def get_phi(self):
|
|
return self.get_euler_angles()[1]
|
|
|
|
def get_gamma(self):
|
|
return self.get_euler_angles()[2]
|
|
|
|
def get_scale(self):
|
|
return self.get_height() / FRAME_SHAPE[1]
|
|
|
|
def get_inverse_camera_rotation_matrix(self):
|
|
return self.get_orientation().as_matrix().T
|
|
|
|
def get_view_matrix(self, refresh=False):
|
|
"""
|
|
Returns a 4x4 for the affine transformation mapping a point
|
|
into the camera's internal coordinate system
|
|
"""
|
|
if self._data_has_changed:
|
|
shift = self.id4x4.copy()
|
|
rotation = self.id4x4.copy()
|
|
|
|
scale = self.get_scale()
|
|
shift[:3, 3] = -self.get_center()
|
|
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
|
|
np.dot(rotation, shift, out=self.view_matrix)
|
|
if scale > 0:
|
|
self.view_matrix[:3, :4] /= scale
|
|
|
|
return self.view_matrix
|
|
|
|
def get_inv_view_matrix(self):
|
|
return np.linalg.inv(self.get_view_matrix())
|
|
|
|
@Mobject.affects_data
|
|
def interpolate(self, *args, **kwargs):
|
|
super().interpolate(*args, **kwargs)
|
|
|
|
@Mobject.affects_data
|
|
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
|
|
rot = Rotation.from_rotvec(angle * normalize(axis))
|
|
self.set_orientation(rot * self.get_orientation())
|
|
return self
|
|
|
|
def set_euler_angles(
|
|
self,
|
|
theta: float | None = None,
|
|
phi: float | None = None,
|
|
gamma: float | None = None,
|
|
units: float = RADIANS
|
|
):
|
|
eulers = self.get_euler_angles() # theta, phi, gamma
|
|
for i, var in enumerate([theta, phi, gamma]):
|
|
if var is not None:
|
|
eulers[i] = var * units
|
|
if all(eulers == 0):
|
|
rot = Rotation.identity()
|
|
else:
|
|
rot = Rotation.from_euler(self.euler_axes, eulers[::-1])
|
|
self.set_orientation(rot)
|
|
return self
|
|
|
|
def increment_euler_angles(
|
|
self,
|
|
dtheta: float = 0,
|
|
dphi: float = 0,
|
|
dgamma: float = 0,
|
|
units: float = RADIANS
|
|
):
|
|
angles = self.get_euler_angles()
|
|
new_angles = angles + np.array([dtheta, dphi, dgamma]) * units
|
|
|
|
# Limit range for phi
|
|
if self.euler_axes == "zxz":
|
|
new_angles[1] = clip(new_angles[1], 0, PI)
|
|
elif self.euler_axes == "zxy":
|
|
new_angles[1] = clip(new_angles[1], -PI / 2, PI / 2)
|
|
|
|
new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1])
|
|
self.set_orientation(new_rot)
|
|
return self
|
|
|
|
def set_euler_axes(self, seq: str):
|
|
self.euler_axes = seq
|
|
|
|
def reorient(
|
|
self,
|
|
theta_degrees: float | None = None,
|
|
phi_degrees: float | None = None,
|
|
gamma_degrees: float | None = None,
|
|
center: Vect3 | tuple[float, float, float] | None = None,
|
|
height: float | None = None
|
|
):
|
|
"""
|
|
Shortcut for set_euler_angles, defaulting to taking
|
|
in angles in degrees
|
|
"""
|
|
self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEG)
|
|
if center is not None:
|
|
self.move_to(np.array(center))
|
|
if height is not None:
|
|
self.set_height(height)
|
|
return self
|
|
|
|
def set_theta(self, theta: float):
|
|
return self.set_euler_angles(theta=theta)
|
|
|
|
def set_phi(self, phi: float):
|
|
return self.set_euler_angles(phi=phi)
|
|
|
|
def set_gamma(self, gamma: float):
|
|
return self.set_euler_angles(gamma=gamma)
|
|
|
|
def increment_theta(self, dtheta: float, units=RADIANS):
|
|
self.increment_euler_angles(dtheta=dtheta, units=units)
|
|
return self
|
|
|
|
def increment_phi(self, dphi: float, units=RADIANS):
|
|
self.increment_euler_angles(dphi=dphi, units=units)
|
|
return self
|
|
|
|
def increment_gamma(self, dgamma: float, units=RADIANS):
|
|
self.increment_euler_angles(dgamma=dgamma, units=units)
|
|
return self
|
|
|
|
def add_ambient_rotation(self, angular_speed=1 * DEG):
|
|
self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
|
|
return self
|
|
|
|
@Mobject.affects_data
|
|
def set_focal_distance(self, focal_distance: float):
|
|
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
|
|
return self
|
|
|
|
@Mobject.affects_data
|
|
def set_field_of_view(self, field_of_view: float):
|
|
self.uniforms["fovy"] = field_of_view
|
|
return self
|
|
|
|
def get_shape(self):
|
|
return (self.get_width(), self.get_height())
|
|
|
|
def get_aspect_ratio(self):
|
|
width, height = self.get_shape()
|
|
return width / height
|
|
|
|
def get_center(self) -> np.ndarray:
|
|
# Assumes first point is at the center
|
|
return self.get_points()[0]
|
|
|
|
def get_width(self) -> float:
|
|
points = self.get_points()
|
|
return points[2, 0] - points[1, 0]
|
|
|
|
def get_height(self) -> float:
|
|
points = self.get_points()
|
|
return points[4, 1] - points[3, 1]
|
|
|
|
def get_focal_distance(self) -> float:
|
|
return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
|
|
|
|
def get_field_of_view(self) -> float:
|
|
return self.uniforms["fovy"]
|
|
|
|
def get_implied_camera_location(self) -> np.ndarray:
|
|
if self._data_has_changed:
|
|
to_camera = self.get_inverse_camera_rotation_matrix()[2]
|
|
dist = self.get_focal_distance()
|
|
self.camera_location = self.get_center() + dist * to_camera
|
|
return self.camera_location
|
|
|
|
def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
|
|
view = self.get_view_matrix()
|
|
point4d = [*point, 0 if relative else 1]
|
|
return np.dot(point4d, view.T)[:3]
|
|
|
|
def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
|
|
inv_view = self.get_inv_view_matrix()
|
|
point4d = [*point, 0 if relative else 1]
|
|
return np.dot(point4d, inv_view.T)[:3]
|