3b1b-manim/manimlib/mobject/three_dimensions.py

407 lines
11 KiB
Python
Raw Normal View History

from __future__ import annotations
2020-06-09 21:24:42 -07:00
import math
2022-04-22 16:42:45 +08:00
import numpy as np
2022-12-16 09:56:38 -08:00
from manimlib.constants import BLUE, BLUE_D, BLUE_E, GREY_A, BLACK
2022-04-12 19:19:59 +08:00
from manimlib.constants import IN, ORIGIN, OUT, RIGHT
from manimlib.constants import PI, TAU
2022-12-16 09:56:38 -08:00
from manimlib.mobject.mobject import Mobject
2020-06-05 13:37:06 -07:00
from manimlib.mobject.types.surface import SGroup
2022-04-12 19:19:59 +08:00
from manimlib.mobject.types.surface import Surface
2020-06-06 13:23:30 -07:00
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
2021-12-13 16:03:46 -08:00
from manimlib.mobject.geometry import Polygon
2022-04-12 19:19:59 +08:00
from manimlib.mobject.geometry import Square
2021-11-08 21:47:26 -08:00
from manimlib.utils.bezier import interpolate
from manimlib.utils.iterables import adjacent_pairs
2022-04-12 19:19:59 +08:00
from manimlib.utils.space_ops import compass_directions
2020-06-05 13:24:26 -07:00
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import z_to_vector
2022-12-16 09:56:38 -08:00
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Tuple, TypeVar
from manimlib.typing import ManimColor, Vect3, Sequence
2022-12-16 09:56:38 -08:00
T = TypeVar("T", bound=Mobject)
2020-06-06 13:23:30 -07:00
class SurfaceMesh(VGroup):
2022-12-16 09:56:38 -08:00
def __init__(
self,
uv_surface: Surface,
resolution: Tuple[int, int] = (21, 11),
stroke_width: float = 1,
stroke_color: ManimColor = GREY_A,
normal_nudge: float = 1e-2,
depth_test: bool = True,
2023-01-10 11:06:41 -08:00
joint_type: str = 'no_joint',
2022-12-16 09:56:38 -08:00
**kwargs
):
2020-06-06 13:23:30 -07:00
self.uv_surface = uv_surface
2022-12-16 09:56:38 -08:00
self.resolution = resolution
self.normal_nudge = normal_nudge
super().__init__(
stroke_color=stroke_color,
stroke_width=stroke_width,
depth_test=depth_test,
2023-01-10 11:06:41 -08:00
joint_type=joint_type,
2022-12-16 09:56:38 -08:00
**kwargs
)
2020-06-06 13:23:30 -07:00
def init_points(self) -> None:
2020-06-06 13:23:30 -07:00
uv_surface = self.uv_surface
full_nu, full_nv = uv_surface.resolution
part_nu, part_nv = self.resolution
2021-11-08 21:47:26 -08:00
# 'indices' are treated as floats. Later, there will be
# an interpolation between the floor and ceiling of these
# indices
u_indices = np.linspace(0, full_nu - 1, part_nu)
v_indices = np.linspace(0, full_nv - 1, part_nv)
2020-06-06 13:23:30 -07:00
points = uv_surface.get_points()
2020-06-06 13:23:30 -07:00
normals = uv_surface.get_unit_normals()
2021-07-28 07:32:16 -07:00
nudge = self.normal_nudge
2020-06-06 13:23:30 -07:00
nudged_points = points + nudge * normals
for ui in u_indices:
path = VMobject()
2021-11-08 21:47:26 -08:00
low_ui = full_nv * int(math.floor(ui))
high_ui = full_nv * int(math.ceil(ui))
path.set_points_smoothly(interpolate(
nudged_points[low_ui:low_ui + full_nv],
nudged_points[high_ui:high_ui + full_nv],
ui % 1
))
2020-06-06 13:23:30 -07:00
self.add(path)
for vi in v_indices:
path = VMobject()
2021-11-08 21:47:26 -08:00
path.set_points_smoothly(interpolate(
nudged_points[int(math.floor(vi))::full_nv],
nudged_points[int(math.ceil(vi))::full_nv],
vi % 1
))
2020-06-06 13:23:30 -07:00
self.add(path)
# 3D shapes
class Sphere(Surface):
2022-12-16 09:56:38 -08:00
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
2024-11-25 12:39:41 -07:00
v_range: Tuple[float, float] = (0, PI),
2022-12-16 09:56:38 -08:00
resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0,
**kwargs,
):
self.radius = radius
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
def uv_func(self, u: float, v: float) -> np.ndarray:
2020-06-04 15:41:20 -07:00
return self.radius * np.array([
2022-12-16 09:56:38 -08:00
math.cos(u) * math.sin(v),
math.sin(u) * math.sin(v),
-math.cos(v)
])
class Torus(Surface):
2022-12-16 09:56:38 -08:00
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (0, TAU),
r1: float = 3.0,
r2: float = 1.0,
**kwargs,
):
self.r1 = r1
self.r2 = r2
super().__init__(
u_range=u_range,
v_range=v_range,
**kwargs,
)
2020-06-17 17:12:23 -07:00
def uv_func(self, u: float, v: float) -> np.ndarray:
2020-06-17 17:12:23 -07:00
P = np.array([math.cos(u), math.sin(u), 0])
2022-12-16 09:56:38 -08:00
return (self.r1 - self.r2 * math.cos(v)) * P - self.r2 * math.sin(v) * OUT
2020-06-17 17:12:23 -07:00
class Cylinder(Surface):
2022-12-16 09:56:38 -08:00
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (-1, 1),
resolution: Tuple[int, int] = (101, 11),
height: float = 2,
radius: float = 1,
axis: Vect3 = OUT,
2022-12-16 09:56:38 -08:00
**kwargs,
):
self.height = height
self.radius = radius
self.axis = axis
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
2020-06-05 13:24:26 -07:00
def init_points(self):
super().init_points()
self.scale(self.radius)
self.set_depth(self.height, stretch=True)
self.apply_matrix(z_to_vector(self.axis))
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([np.cos(u), np.sin(u), v])
2020-06-05 13:24:26 -07:00
2024-11-08 14:28:17 -06:00
class Cone(Cylinder):
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (0, 1),
*args,
**kwargs,
):
super().__init__(u_range=u_range, v_range=v_range, *args, **kwargs)
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([(1 - v) * np.cos(u), (1 - v) * np.sin(u), v])
2020-06-05 13:24:26 -07:00
class Line3D(Cylinder):
2022-12-16 09:56:38 -08:00
def __init__(
self,
start: Vect3,
end: Vect3,
2022-12-16 09:56:38 -08:00
width: float = 0.05,
resolution: Tuple[int, int] = (21, 25),
**kwargs
):
2020-06-05 13:24:26 -07:00
axis = end - start
super().__init__(
height=get_norm(axis),
2022-12-16 09:56:38 -08:00
radius=width / 2,
axis=axis,
2023-01-31 14:19:24 -08:00
resolution=resolution,
2022-12-16 09:56:38 -08:00
**kwargs
2020-06-05 13:24:26 -07:00
)
2020-06-17 17:12:23 -07:00
self.shift((start + end) / 2)
class Disk3D(Surface):
2022-12-16 09:56:38 -08:00
def __init__(
self,
radius: float = 1,
u_range: Tuple[float, float] = (0, 1),
v_range: Tuple[float, float] = (0, TAU),
resolution: Tuple[int, int] = (2, 100),
**kwargs
):
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs,
)
self.scale(radius)
2020-06-05 13:24:26 -07:00
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([
2022-12-16 09:56:38 -08:00
u * math.cos(v),
u * math.sin(v),
2020-06-05 13:24:26 -07:00
0
])
2020-06-05 13:24:26 -07:00
class Square3D(Surface):
2022-12-16 09:56:38 -08:00
def __init__(
self,
side_length: float = 2.0,
u_range: Tuple[float, float] = (-1, 1),
v_range: Tuple[float, float] = (-1, 1),
resolution: Tuple[int, int] = (2, 2),
**kwargs,
):
super().__init__(
u_range=u_range,
v_range=v_range,
resolution=resolution,
**kwargs
)
self.scale(side_length / 2)
2020-06-05 13:37:06 -07:00
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([u, v, 0])
2020-06-05 13:37:06 -07:00
2022-12-16 09:56:38 -08:00
def square_to_cube_faces(square: T) -> list[T]:
radius = square.get_height() / 2
square.move_to(radius * OUT)
result = [square.copy()]
result.extend([
square.copy().rotate(PI / 2, axis=vect, about_point=ORIGIN)
for vect in compass_directions(4)
])
result.append(square.copy().rotate(PI, RIGHT, about_point=ORIGIN))
return result
2022-12-16 09:56:38 -08:00
class Cube(SGroup):
def __init__(
self,
color: ManimColor = BLUE,
opacity: float = 1,
shading: Tuple[float, float, float] = (0.1, 0.5, 0.1),
2022-12-16 09:56:38 -08:00
square_resolution: Tuple[int, int] = (2, 2),
side_length: float = 2,
**kwargs,
):
2021-08-19 08:37:01 -07:00
face = Square3D(
2022-12-16 09:56:38 -08:00
resolution=square_resolution,
side_length=side_length,
color=color,
opacity=opacity,
shading=shading,
2022-12-16 09:56:38 -08:00
)
super().__init__(*square_to_cube_faces(face), **kwargs)
2021-08-19 08:37:01 -07:00
2022-03-29 20:34:14 -07:00
class Prism(Cube):
2022-12-16 09:56:38 -08:00
def __init__(
self,
width: float = 3.0,
height: float = 2.0,
depth: float = 1.0,
**kwargs
):
2022-03-29 20:34:14 -07:00
super().__init__(**kwargs)
for dim, value in enumerate([width, height, depth]):
self.rescale_to_fit(value, dim, stretch=True)
2022-12-16 09:56:38 -08:00
class VGroup3D(VGroup):
def __init__(
self,
*vmobjects: VMobject,
depth_test: bool = True,
shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
2023-01-10 11:06:41 -08:00
joint_type: str = "no_joint",
2022-12-16 09:56:38 -08:00
**kwargs
):
super().__init__(*vmobjects, **kwargs)
self.set_shading(*shading)
2022-12-16 09:56:38 -08:00
self.set_joint_type(joint_type)
if depth_test:
self.apply_depth_test()
class VCube(VGroup3D):
def __init__(
self,
side_length: float = 2.0,
fill_color: ManimColor = BLUE_D,
fill_opacity: float = 1,
stroke_width: float = 0,
**kwargs
):
style = dict(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
**kwargs
)
face = Square(side_length=side_length, **style)
super().__init__(*square_to_cube_faces(face), **style)
2022-03-29 20:34:14 -07:00
class VPrism(VCube):
2022-12-16 09:56:38 -08:00
def __init__(
self,
width: float = 3.0,
height: float = 2.0,
depth: float = 1.0,
**kwargs
):
2022-03-29 20:34:14 -07:00
super().__init__(**kwargs)
for dim, value in enumerate([width, height, depth]):
self.rescale_to_fit(value, dim, stretch=True)
2022-12-16 09:56:38 -08:00
class Dodecahedron(VGroup3D):
def __init__(
self,
fill_color: ManimColor = BLUE_E,
fill_opacity: float = 1,
stroke_color: ManimColor = BLUE_E,
stroke_width: float = 1,
shading: Tuple[float, float, float] = (0.2, 0.2, 0.2),
2022-12-16 09:56:38 -08:00
**kwargs,
):
style = dict(
fill_color=fill_color,
fill_opacity=fill_opacity,
stroke_color=stroke_color,
stroke_width=stroke_width,
shading=shading,
2022-12-16 09:56:38 -08:00
**kwargs
)
2021-12-13 16:03:46 -08:00
2022-12-16 09:56:38 -08:00
# Start by creating two of the pentagons, meeting
2021-12-13 16:03:46 -08:00
# back to back on the positive x-axis
phi = (1 + math.sqrt(5)) / 2
x, y, z = np.identity(3)
pentagon1 = Polygon(
2022-12-16 09:56:38 -08:00
np.array([phi, 1 / phi, 0]),
np.array([1, 1, 1]),
np.array([1 / phi, 0, phi]),
np.array([1, -1, 1]),
np.array([phi, -1 / phi, 0]),
**style
2021-12-13 16:03:46 -08:00
)
pentagon2 = pentagon1.copy().stretch(-1, 2, about_point=ORIGIN)
pentagon2.reverse_points()
x_pair = VGroup(pentagon1, pentagon2)
z_pair = x_pair.copy().apply_matrix(np.array([z, -x, -y]).T)
y_pair = x_pair.copy().apply_matrix(np.array([y, z, x]).T)
2022-12-16 09:56:38 -08:00
pentagons = [*x_pair, *y_pair, *z_pair]
for pentagon in list(pentagons):
2021-12-13 16:03:46 -08:00
pc = pentagon.copy()
pc.apply_function(lambda p: -p)
pc.reverse_points()
2022-12-16 09:56:38 -08:00
pentagons.append(pc)
super().__init__(*pentagons, **style)
2021-12-13 16:03:46 -08:00
2022-12-16 09:56:38 -08:00
class Prismify(VGroup3D):
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
# At the moment, this assume stright edges
vect = depth * direction
2022-12-16 09:56:38 -08:00
pieces = [vmobject.copy()]
2023-01-11 19:52:45 -08:00
points = vmobject.get_anchors()
for p1, p2 in adjacent_pairs(points):
wall = VMobject()
wall.match_style(vmobject)
wall.set_points_as_corners([p1, p2, p2 + vect, p1 + vect])
2022-12-16 09:56:38 -08:00
pieces.append(wall)
2023-01-11 19:52:45 -08:00
top = vmobject.copy()
top.shift(vect)
top.reverse_points()
pieces.append(top)
2022-12-16 09:56:38 -08:00
super().__init__(*pieces, **kwargs)