three_dimensions.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. from __future__ import annotations
  2. import math
  3. import numpy as np
  4. from manimlib.constants import BLUE, BLUE_D, BLUE_E, GREY_A, BLACK
  5. from manimlib.constants import IN, ORIGIN, OUT, RIGHT
  6. from manimlib.constants import PI, TAU
  7. from manimlib.mobject.mobject import Mobject
  8. from manimlib.mobject.types.surface import SGroup
  9. from manimlib.mobject.types.surface import Surface
  10. from manimlib.mobject.types.vectorized_mobject import VGroup
  11. from manimlib.mobject.types.vectorized_mobject import VMobject
  12. from manimlib.mobject.geometry import Polygon
  13. from manimlib.mobject.geometry import Square
  14. from manimlib.utils.bezier import interpolate
  15. from manimlib.utils.iterables import adjacent_pairs
  16. from manimlib.utils.space_ops import compass_directions
  17. from manimlib.utils.space_ops import get_norm
  18. from manimlib.utils.space_ops import z_to_vector
  19. from typing import TYPE_CHECKING
  20. if TYPE_CHECKING:
  21. from typing import Tuple, TypeVar
  22. from manimlib.typing import ManimColor, Vect3, Sequence
  23. T = TypeVar("T", bound=Mobject)
  24. class SurfaceMesh(VGroup):
  25. def __init__(
  26. self,
  27. uv_surface: Surface,
  28. resolution: Tuple[int, int] = (21, 11),
  29. stroke_width: float = 1,
  30. stroke_color: ManimColor = GREY_A,
  31. normal_nudge: float = 1e-2,
  32. depth_test: bool = True,
  33. joint_type: str = 'no_joint',
  34. **kwargs
  35. ):
  36. self.uv_surface = uv_surface
  37. self.resolution = resolution
  38. self.normal_nudge = normal_nudge
  39. super().__init__(
  40. stroke_color=stroke_color,
  41. stroke_width=stroke_width,
  42. depth_test=depth_test,
  43. joint_type=joint_type,
  44. **kwargs
  45. )
  46. def init_points(self) -> None:
  47. uv_surface = self.uv_surface
  48. full_nu, full_nv = uv_surface.resolution
  49. part_nu, part_nv = self.resolution
  50. # 'indices' are treated as floats. Later, there will be
  51. # an interpolation between the floor and ceiling of these
  52. # indices
  53. u_indices = np.linspace(0, full_nu - 1, part_nu)
  54. v_indices = np.linspace(0, full_nv - 1, part_nv)
  55. points = uv_surface.get_points()
  56. normals = uv_surface.get_unit_normals()
  57. nudge = self.normal_nudge
  58. nudged_points = points + nudge * normals
  59. for ui in u_indices:
  60. path = VMobject()
  61. low_ui = full_nv * int(math.floor(ui))
  62. high_ui = full_nv * int(math.ceil(ui))
  63. path.set_points_smoothly(interpolate(
  64. nudged_points[low_ui:low_ui + full_nv],
  65. nudged_points[high_ui:high_ui + full_nv],
  66. ui % 1
  67. ))
  68. self.add(path)
  69. for vi in v_indices:
  70. path = VMobject()
  71. path.set_points_smoothly(interpolate(
  72. nudged_points[int(math.floor(vi))::full_nv],
  73. nudged_points[int(math.ceil(vi))::full_nv],
  74. vi % 1
  75. ))
  76. self.add(path)
  77. # 3D shapes
  78. class Sphere(Surface):
  79. def __init__(
  80. self,
  81. u_range: Tuple[float, float] = (0, TAU),
  82. v_range: Tuple[float, float] = (1e-5, PI - 1e-5),
  83. resolution: Tuple[int, int] = (101, 51),
  84. radius: float = 1.0,
  85. **kwargs,
  86. ):
  87. self.radius = radius
  88. super().__init__(
  89. u_range=u_range,
  90. v_range=v_range,
  91. resolution=resolution,
  92. **kwargs
  93. )
  94. def uv_func(self, u: float, v: float) -> np.ndarray:
  95. return self.radius * np.array([
  96. math.cos(u) * math.sin(v),
  97. math.sin(u) * math.sin(v),
  98. -math.cos(v)
  99. ])
  100. class Torus(Surface):
  101. def __init__(
  102. self,
  103. u_range: Tuple[float, float] = (0, TAU),
  104. v_range: Tuple[float, float] = (0, TAU),
  105. r1: float = 3.0,
  106. r2: float = 1.0,
  107. **kwargs,
  108. ):
  109. self.r1 = r1
  110. self.r2 = r2
  111. super().__init__(
  112. u_range=u_range,
  113. v_range=v_range,
  114. **kwargs,
  115. )
  116. def uv_func(self, u: float, v: float) -> np.ndarray:
  117. P = np.array([math.cos(u), math.sin(u), 0])
  118. return (self.r1 - self.r2 * math.cos(v)) * P - self.r2 * math.sin(v) * OUT
  119. class Cylinder(Surface):
  120. def __init__(
  121. self,
  122. u_range: Tuple[float, float] = (0, TAU),
  123. v_range: Tuple[float, float] = (-1, 1),
  124. resolution: Tuple[int, int] = (101, 11),
  125. height: float = 2,
  126. radius: float = 1,
  127. axis: Vect3 = OUT,
  128. **kwargs,
  129. ):
  130. self.height = height
  131. self.radius = radius
  132. self.axis = axis
  133. super().__init__(
  134. u_range=u_range,
  135. v_range=v_range,
  136. resolution=resolution,
  137. **kwargs
  138. )
  139. def init_points(self):
  140. super().init_points()
  141. self.scale(self.radius)
  142. self.set_depth(self.height, stretch=True)
  143. self.apply_matrix(z_to_vector(self.axis))
  144. def uv_func(self, u: float, v: float) -> np.ndarray:
  145. return np.array([np.cos(u), np.sin(u), v])
  146. class Line3D(Cylinder):
  147. def __init__(
  148. self,
  149. start: Vect3,
  150. end: Vect3,
  151. width: float = 0.05,
  152. resolution: Tuple[int, int] = (21, 25),
  153. **kwargs
  154. ):
  155. axis = end - start
  156. super().__init__(
  157. height=get_norm(axis),
  158. radius=width / 2,
  159. axis=axis,
  160. resolution=resolution,
  161. **kwargs
  162. )
  163. self.shift((start + end) / 2)
  164. class Disk3D(Surface):
  165. def __init__(
  166. self,
  167. radius: float = 1,
  168. u_range: Tuple[float, float] = (0, 1),
  169. v_range: Tuple[float, float] = (0, TAU),
  170. resolution: Tuple[int, int] = (2, 100),
  171. **kwargs
  172. ):
  173. super().__init__(
  174. u_range=u_range,
  175. v_range=v_range,
  176. resolution=resolution,
  177. **kwargs,
  178. )
  179. self.scale(radius)
  180. def uv_func(self, u: float, v: float) -> np.ndarray:
  181. return np.array([
  182. u * math.cos(v),
  183. u * math.sin(v),
  184. 0
  185. ])
  186. class Square3D(Surface):
  187. def __init__(
  188. self,
  189. side_length: float = 2.0,
  190. u_range: Tuple[float, float] = (-1, 1),
  191. v_range: Tuple[float, float] = (-1, 1),
  192. resolution: Tuple[int, int] = (2, 2),
  193. **kwargs,
  194. ):
  195. super().__init__(
  196. u_range=u_range,
  197. v_range=v_range,
  198. resolution=resolution,
  199. **kwargs
  200. )
  201. self.scale(side_length / 2)
  202. def uv_func(self, u: float, v: float) -> np.ndarray:
  203. return np.array([u, v, 0])
  204. def square_to_cube_faces(square: T) -> list[T]:
  205. radius = square.get_height() / 2
  206. square.move_to(radius * OUT)
  207. result = [square.copy()]
  208. result.extend([
  209. square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
  210. for vect in compass_directions(4)
  211. ])
  212. result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
  213. return result
  214. class Cube(SGroup):
  215. def __init__(
  216. self,
  217. color: ManimColor = BLUE,
  218. opacity: float = 1,
  219. shading: Tuple[float, float, float] = (0.1, 0.5, 0.1),
  220. square_resolution: Tuple[int, int] = (2, 2),
  221. side_length: float = 2,
  222. **kwargs,
  223. ):
  224. face = Square3D(
  225. resolution=square_resolution,
  226. side_length=side_length,
  227. color=color,
  228. opacity=opacity,
  229. shading=shading,
  230. )
  231. super().__init__(*square_to_cube_faces(face), **kwargs)
  232. class Prism(Cube):
  233. def __init__(
  234. self,
  235. width: float = 3.0,
  236. height: float = 2.0,
  237. depth: float = 1.0,
  238. **kwargs
  239. ):
  240. super().__init__(**kwargs)
  241. for dim, value in enumerate([width, height, depth]):
  242. self.rescale_to_fit(value, dim, stretch=True)
  243. class VGroup3D(VGroup):
  244. def __init__(
  245. self,
  246. *vmobjects: VMobject,
  247. depth_test: bool = True,
  248. shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
  249. joint_type: str = "no_joint",
  250. **kwargs
  251. ):
  252. super().__init__(*vmobjects, **kwargs)
  253. self.set_shading(*shading)
  254. self.set_joint_type(joint_type)
  255. if depth_test:
  256. self.apply_depth_test()
  257. class VCube(VGroup3D):
  258. def __init__(
  259. self,
  260. side_length: float = 2.0,
  261. fill_color: ManimColor = BLUE_D,
  262. fill_opacity: float = 1,
  263. stroke_width: float = 0,
  264. **kwargs
  265. ):
  266. style = dict(
  267. fill_color=fill_color,
  268. fill_opacity=fill_opacity,
  269. stroke_width=stroke_width,
  270. **kwargs
  271. )
  272. face = Square(side_length=side_length, **style)
  273. super().__init__(*square_to_cube_faces(face), **style)
  274. class VPrism(VCube):
  275. def __init__(
  276. self,
  277. width: float = 3.0,
  278. height: float = 2.0,
  279. depth: float = 1.0,
  280. **kwargs
  281. ):
  282. super().__init__(**kwargs)
  283. for dim, value in enumerate([width, height, depth]):
  284. self.rescale_to_fit(value, dim, stretch=True)
  285. class Dodecahedron(VGroup3D):
  286. def __init__(
  287. self,
  288. fill_color: ManimColor = BLUE_E,
  289. fill_opacity: float = 1,
  290. stroke_color: ManimColor = BLUE_E,
  291. stroke_width: float = 1,
  292. shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
  293. **kwargs,
  294. ):
  295. style = dict(
  296. fill_color=fill_color,
  297. fill_opacity=fill_opacity,
  298. stroke_color=stroke_color,
  299. stroke_width=stroke_width,
  300. shading=shading,
  301. **kwargs
  302. )
  303. # Start by creating two of the pentagons, meeting
  304. # back to back on the positive x-axis
  305. phi = (1 + math.sqrt(5)) / 2
  306. x, y, z = np.identity(3)
  307. pentagon1 = Polygon(
  308. np.array([phi, 1 / phi, 0]),
  309. np.array([1, 1, 1]),
  310. np.array([1 / phi, 0, phi]),
  311. np.array([1, -1, 1]),
  312. np.array([phi, -1 / phi, 0]),
  313. **style
  314. )
  315. pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
  316. pentagon2.reverse_points()
  317. x_pair = VGroup(pentagon1, pentagon2)
  318. z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
  319. y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
  320. pentagons = [*x_pair, *y_pair, *z_pair]
  321. for pentagon in list(pentagons):
  322. pc = pentagon.copy()
  323. pc.apply_function(lambda p: -p)
  324. pc.reverse_points()
  325. pentagons.append(pc)
  326. super().__init__(*pentagons, **style)
  327. class Prismify(VGroup3D):
  328. def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
  329. # At the moment, this assume stright edges
  330. vect = depth * direction
  331. pieces = [vmobject.copy()]
  332. points = vmobject.get_anchors()
  333. for p1, p2 in adjacent_pairs(points):
  334. wall = VMobject()
  335. wall.match_style(vmobject)
  336. wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
  337. pieces.append(wall)
  338. top = vmobject.copy()
  339. top.shift(vect)
  340. top.reverse_points()
  341. pieces.append(top)
  342. super().__init__(*pieces, **kwargs)