3b1b-manim/manimlib/mobject/three_dimensions.py
2021-12-21 10:58:41 -08:00

280 lines
7.6 KiB
Python

import math
from manimlib.constants import *
from manimlib.mobject.types.surface import Surface
from manimlib.mobject.types.surface import SGroup
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.geometry import Square
from manimlib.mobject.geometry import Polygon
from manimlib.utils.bezier import interpolate
from manimlib.utils.config_ops import digest_config
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import z_to_vector
from manimlib.utils.space_ops import compass_directions
class SurfaceMesh(VGroup):
CONFIG = {
"resolution": (21, 11),
"stroke_width": 1,
"normal_nudge": 1e-2,
"depth_test": True,
"flat_stroke": False,
}
def __init__(self, uv_surface, **kwargs):
if not isinstance(uv_surface, Surface):
raise Exception("uv_surface must be of type Surface")
self.uv_surface = uv_surface
super().__init__(**kwargs)
def init_points(self):
uv_surface = self.uv_surface
full_nu, full_nv = uv_surface.resolution
part_nu, part_nv = self.resolution
# '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)
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
normals = uv_surface.get_unit_normals()
nudge = self.normal_nudge
nudged_points = points + nudge * normals
for ui in u_indices:
path = VMobject()
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
))
self.add(path)
for vi in v_indices:
path = VMobject()
path.set_points_smoothly(interpolate(
nudged_points[int(math.floor(vi))::full_nv],
nudged_points[int(math.ceil(vi))::full_nv],
vi % 1
))
self.add(path)
# 3D shapes
class Sphere(Surface):
CONFIG = {
"resolution": (101, 51),
"radius": 1,
"u_range": (0, TAU),
"v_range": (0, PI),
}
def uv_func(self, u, v):
return self.radius * np.array([
np.cos(u) * np.sin(v),
np.sin(u) * np.sin(v),
-np.cos(v)
])
class Torus(Surface):
CONFIG = {
"u_range": (0, TAU),
"v_range": (0, TAU),
"r1": 3,
"r2": 1,
}
def uv_func(self, u, v):
P = np.array([math.cos(u), math.sin(u), 0])
return (self.r1 - self.r2 * math.cos(v)) * P - math.sin(v) * OUT
class Cylinder(Surface):
CONFIG = {
"height": 2,
"radius": 1,
"axis": OUT,
"u_range": (0, TAU),
"v_range": (-1, 1),
"resolution": (101, 11),
}
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))
return self
def uv_func(self, u, v):
return [np.cos(u), np.sin(u), v]
class Line3D(Cylinder):
CONFIG = {
"width": 0.05,
"resolution": (21, 25)
}
def __init__(self, start, end, **kwargs):
digest_config(self, kwargs)
axis = end - start
super().__init__(
height=get_norm(axis),
radius=self.width / 2,
axis=axis
)
self.shift((start + end) / 2)
class Disk3D(Surface):
CONFIG = {
"radius": 1,
"u_range": (0, 1),
"v_range": (0, TAU),
"resolution": (2, 25),
}
def init_points(self):
super().init_points()
self.scale(self.radius)
def uv_func(self, u, v):
return [
u * np.cos(v),
u * np.sin(v),
0
]
class Square3D(Surface):
CONFIG = {
"side_length": 2,
"u_range": (-1, 1),
"v_range": (-1, 1),
"resolution": (2, 2),
}
def init_points(self):
super().init_points()
self.scale(self.side_length / 2)
def uv_func(self, u, v):
return [u, v, 0]
class Cube(SGroup):
CONFIG = {
"color": BLUE,
"opacity": 1,
"gloss": 0.5,
"square_resolution": (2, 2),
"side_length": 2,
"square_class": Square3D,
}
def init_points(self):
face = Square3D(
resolution=self.square_resolution,
side_length=self.side_length,
)
self.add(*self.square_to_cube_faces(face))
@staticmethod
def square_to_cube_faces(square):
radius = square.get_height() / 2
square.move_to(radius * OUT)
result = [square]
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
def _get_face(self):
return Square3D(resolution=self.square_resolution)
class VCube(VGroup):
CONFIG = {
"fill_color": BLUE_D,
"fill_opacity": 1,
"stroke_width": 0,
"gloss": 0.5,
"shadow": 0.5,
}
def __init__(self, side_length=2, **kwargs):
super().__init__(**kwargs)
face = Square(side_length=side_length)
face.get_triangulation()
self.add(*Cube.square_to_cube_faces(face))
self.init_colors()
self.apply_depth_test()
self.refresh_unit_normal()
class Dodecahedron(VGroup):
CONFIG = {
"fill_color": BLUE_E,
"fill_opacity": 1,
"stroke_width": 1,
"reflectiveness": 0.2,
"gloss": 0.3,
"shadow": 0.2,
"depth_test": True,
}
def init_points(self):
# Star by creating two of the pentagons, meeting
# back to back on the positive x-axis
phi = (1 + math.sqrt(5)) / 2
x, y, z = np.identity(3)
pentagon1 = Polygon(
[phi, 1 / phi, 0],
[1, 1, 1],
[1 / phi, 0, phi],
[1, -1, 1],
[phi, -1 / phi, 0],
)
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)
self.add(*x_pair, *y_pair, *z_pair)
for pentagon in list(self):
pc = pentagon.copy()
pc.apply_function(lambda p: -p)
pc.reverse_points()
self.add(pc)
# # Rotate those two pentagons by all the axis permuations to fill
# # out the dodecahedron
# Id = np.identity(3)
# for i in range(3):
# perm = [j % 3 for j in range(i, i + 3)]
# for b in [1, -1]:
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
class Prism(Cube):
CONFIG = {
"dimensions": [3, 2, 1]
}
def init_points(self):
Cube.init_points(self)
for dim, value in enumerate(self.dimensions):
self.rescale_to_fit(value, dim, stretch=True)