camera_frame.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. from __future__ import annotations
  2. import math
  3. import warnings
  4. import numpy as np
  5. from scipy.spatial.transform import Rotation
  6. from pyrr import Matrix44
  7. from manimlib.constants import DEGREES, RADIANS
  8. from manimlib.constants import FRAME_SHAPE
  9. from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
  10. from manimlib.constants import PI
  11. from manimlib.mobject.mobject import Mobject
  12. from manimlib.utils.space_ops import normalize
  13. from manimlib.utils.simple_functions import clip
  14. from typing import TYPE_CHECKING
  15. if TYPE_CHECKING:
  16. from manimlib.typing import Vect3
  17. class CameraFrame(Mobject):
  18. def __init__(
  19. self,
  20. frame_shape: tuple[float, float] = FRAME_SHAPE,
  21. center_point: Vect3 = ORIGIN,
  22. # Field of view in the y direction
  23. fovy: float = 45 * DEGREES,
  24. euler_axes: str = "zxz",
  25. # This keeps it ordered first in a scene
  26. z_index=-1,
  27. **kwargs,
  28. ):
  29. super().__init__(z_index=z_index, **kwargs)
  30. self.uniforms["orientation"] = Rotation.identity().as_quat()
  31. self.uniforms["fovy"] = fovy
  32. self.default_orientation = Rotation.identity()
  33. self.view_matrix = np.identity(4)
  34. self.id4x4 = np.identity(4)
  35. self.camera_location = OUT # This will be updated by set_points
  36. self.euler_axes = euler_axes
  37. self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
  38. self.set_width(frame_shape[0], stretch=True)
  39. self.set_height(frame_shape[1], stretch=True)
  40. self.move_to(center_point)
  41. def set_orientation(self, rotation: Rotation):
  42. self.uniforms["orientation"][:] = rotation.as_quat()
  43. return self
  44. def get_orientation(self):
  45. return Rotation.from_quat(self.uniforms["orientation"])
  46. def make_orientation_default(self):
  47. self.default_orientation = self.get_orientation()
  48. return self
  49. def to_default_state(self):
  50. self.set_shape(*FRAME_SHAPE)
  51. self.center()
  52. self.set_orientation(self.default_orientation)
  53. return self
  54. def get_euler_angles(self) -> np.ndarray:
  55. orientation = self.get_orientation()
  56. if np.isclose(orientation.as_quat(), [0, 0, 0, 1]).all():
  57. return np.zeros(3)
  58. with warnings.catch_warnings():
  59. warnings.simplefilter('ignore', UserWarning) # Ignore UserWarnings
  60. angles = orientation.as_euler(self.euler_axes)[::-1]
  61. # Handle Gimble lock case
  62. if self.euler_axes == "zxz":
  63. if np.isclose(angles[1], 0, atol=1e-2):
  64. angles[0] = angles[0] + angles[2]
  65. angles[2] = 0
  66. if np.isclose(angles[1], PI, atol=1e-2):
  67. angles[0] = angles[0] - angles[2]
  68. angles[2] = 0
  69. return angles
  70. def get_theta(self):
  71. return self.get_euler_angles()[0]
  72. def get_phi(self):
  73. return self.get_euler_angles()[1]
  74. def get_gamma(self):
  75. return self.get_euler_angles()[2]
  76. def get_scale(self):
  77. return self.get_height() / FRAME_SHAPE[1]
  78. def get_inverse_camera_rotation_matrix(self):
  79. return self.get_orientation().as_matrix().T
  80. def get_view_matrix(self, refresh=False):
  81. """
  82. Returns a 4x4 for the affine transformation mapping a point
  83. into the camera's internal coordinate system
  84. """
  85. if self._data_has_changed:
  86. shift = self.id4x4.copy()
  87. rotation = self.id4x4.copy()
  88. scale = self.get_scale()
  89. shift[:3, 3] = -self.get_center()
  90. rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
  91. np.dot(rotation, shift, out=self.view_matrix)
  92. if scale > 0:
  93. self.view_matrix[:3, :4] /= scale
  94. return self.view_matrix
  95. def get_inv_view_matrix(self):
  96. return np.linalg.inv(self.get_view_matrix())
  97. @Mobject.affects_data
  98. def interpolate(self, *args, **kwargs):
  99. super().interpolate(*args, **kwargs)
  100. @Mobject.affects_data
  101. def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
  102. rot = Rotation.from_rotvec(angle * normalize(axis))
  103. self.set_orientation(rot * self.get_orientation())
  104. return self
  105. def set_euler_angles(
  106. self,
  107. theta: float | None = None,
  108. phi: float | None = None,
  109. gamma: float | None = None,
  110. units: float = RADIANS
  111. ):
  112. eulers = self.get_euler_angles() # theta, phi, gamma
  113. for i, var in enumerate([theta, phi, gamma]):
  114. if var is not None:
  115. eulers[i] = var * units
  116. if all(eulers == 0):
  117. rot = Rotation.identity()
  118. else:
  119. rot = Rotation.from_euler(self.euler_axes, eulers[::-1])
  120. self.set_orientation(rot)
  121. return self
  122. def increment_euler_angles(
  123. self,
  124. dtheta: float = 0,
  125. dphi: float = 0,
  126. dgamma: float = 0,
  127. units: float = RADIANS
  128. ):
  129. angles = self.get_euler_angles()
  130. new_angles = angles + np.array([dtheta, dphi, dgamma]) * units
  131. # Limit range for phi
  132. if self.euler_axes == "zxz":
  133. new_angles[1] = clip(new_angles[1], 0, PI)
  134. elif self.euler_axes == "zxy":
  135. new_angles[1] = clip(new_angles[1], -PI / 2, PI / 2)
  136. new_rot = Rotation.from_euler(self.euler_axes, new_angles[::-1])
  137. self.set_orientation(new_rot)
  138. return self
  139. def set_euler_axes(self, seq: str):
  140. self.euler_axes = seq
  141. def reorient(
  142. self,
  143. theta_degrees: float | None = None,
  144. phi_degrees: float | None = None,
  145. gamma_degrees: float | None = None,
  146. center: Vect3 | tuple[float, float, float] | None = None,
  147. height: float | None = None
  148. ):
  149. """
  150. Shortcut for set_euler_angles, defaulting to taking
  151. in angles in degrees
  152. """
  153. self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES)
  154. if center is not None:
  155. self.move_to(np.array(center))
  156. if height is not None:
  157. self.set_height(height)
  158. return self
  159. def set_theta(self, theta: float):
  160. return self.set_euler_angles(theta=theta)
  161. def set_phi(self, phi: float):
  162. return self.set_euler_angles(phi=phi)
  163. def set_gamma(self, gamma: float):
  164. return self.set_euler_angles(gamma=gamma)
  165. def increment_theta(self, dtheta: float, units=RADIANS):
  166. self.increment_euler_angles(dtheta=dtheta, units=units)
  167. return self
  168. def increment_phi(self, dphi: float, units=RADIANS):
  169. self.increment_euler_angles(dphi=dphi, units=units)
  170. return self
  171. def increment_gamma(self, dgamma: float, units=RADIANS):
  172. self.increment_euler_angles(dgamma=dgamma, units=units)
  173. return self
  174. def add_ambient_rotation(self, angular_speed=1 * DEGREES):
  175. self.add_updater(lambda m, dt: m.increment_theta(angular_speed * dt))
  176. return self
  177. @Mobject.affects_data
  178. def set_focal_distance(self, focal_distance: float):
  179. self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
  180. return self
  181. @Mobject.affects_data
  182. def set_field_of_view(self, field_of_view: float):
  183. self.uniforms["fovy"] = field_of_view
  184. return self
  185. def get_shape(self):
  186. return (self.get_width(), self.get_height())
  187. def get_aspect_ratio(self):
  188. width, height = self.get_shape()
  189. return width / height
  190. def get_center(self) -> np.ndarray:
  191. # Assumes first point is at the center
  192. return self.get_points()[0]
  193. def get_width(self) -> float:
  194. points = self.get_points()
  195. return points[2, 0] - points[1, 0]
  196. def get_height(self) -> float:
  197. points = self.get_points()
  198. return points[4, 1] - points[3, 1]
  199. def get_focal_distance(self) -> float:
  200. return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
  201. def get_field_of_view(self) -> float:
  202. return self.uniforms["fovy"]
  203. def get_implied_camera_location(self) -> np.ndarray:
  204. if self._data_has_changed:
  205. to_camera = self.get_inverse_camera_rotation_matrix()[2]
  206. dist = self.get_focal_distance()
  207. self.camera_location = self.get_center() + dist * to_camera
  208. return self.camera_location
  209. def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
  210. view = self.get_view_matrix()
  211. point4d = [*point, 0 if relative else 1]
  212. return np.dot(point4d, view.T)[:3]
  213. def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
  214. inv_view = self.get_inv_view_matrix()
  215. point4d = [*point, 0 if relative else 1]
  216. return np.dot(point4d, inv_view.T)[:3]