3b1b-manim/manimlib/utils/space_ops.py

461 lines
12 KiB
Python
Raw Normal View History

import numpy as np
2020-02-21 10:57:10 -08:00
import math
import itertools as it
from mapbox_earcut import triangulate_float32 as earcut
from manimlib.constants import OUT
from manimlib.constants import PI
from manimlib.constants import RIGHT
from manimlib.constants import TAU
from manimlib.utils.iterables import adjacent_pairs
2018-08-15 17:30:24 -07:00
def get_norm(vect):
return sum([x**2 for x in vect])**0.5
# Quaternions
# TODO, implement quaternion type
def quaternion_mult(*quats):
if len(quats) == 0:
return [1, 0, 0, 0]
result = quats[0]
for next_quat in quats[1:]:
w1, x1, y1, z1 = result
w2, x2, y2, z2 = next_quat
result = [
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2,
w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2,
]
return result
def quaternion_from_angle_axis(angle, axis):
return [
2020-02-21 10:57:10 -08:00
math.cos(angle / 2),
*(math.sin(angle / 2) * normalize(axis))
]
2018-09-27 17:35:40 -07:00
def angle_axis_from_quaternion(quaternion):
axis = normalize(
quaternion[1:],
fall_back=[1, 0, 0]
2018-09-27 17:35:40 -07:00
)
angle = 2 * np.arccos(quaternion[0])
if angle > TAU / 2:
angle = TAU - angle
return angle, axis
def quaternion_conjugate(quaternion):
result = list(quaternion)
for i in range(1, len(result)):
result[i] *= -1
return result
def rotate_vector(vector, angle, axis=OUT):
if len(vector) == 2:
# Use complex numbers...because why not
z = complex(*vector) * np.exp(complex(0, angle))
result = [z.real, z.imag]
elif len(vector) == 3:
# Use quaternions...because why not
quat = quaternion_from_angle_axis(angle, axis)
quat_inv = quaternion_conjugate(quat)
product = quaternion_mult(quat, [0, *vector], quat_inv)
result = product[1:]
else:
raise Exception("vector must be of dimension 2 or 3")
if isinstance(vector, np.ndarray):
return np.array(result)
return result
def thick_diagonal(dim, thickness=2):
row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
col_indices = np.transpose(row_indices)
return (np.abs(row_indices - col_indices) < thickness).astype('uint8')
2020-06-01 16:21:18 -07:00
def rotation_matrix_transpose_from_quaternion(quat):
quat_inv = quaternion_conjugate(quat)
return [
quaternion_mult(quat, [0, *basis], quat_inv)[1:]
for basis in [
[1, 0, 0],
[0, 1, 0],
[0, 0, 1],
]
]
def rotation_matrix_from_quaternion(quat):
return np.transpose(rotation_matrix_transpose_from_quaternion(quat))
def rotation_matrix_transpose(angle, axis):
if axis[0] == 0 and axis[1] == 0:
# axis = [0, 0, z] case is common enough it's worth
2020-02-20 15:51:26 -08:00
# having a shortcut
sgn = 1 if axis[2] > 0 else -1
2020-02-21 10:57:10 -08:00
cos_a = math.cos(angle)
sin_a = math.sin(angle) * sgn
return [
2020-02-20 15:51:26 -08:00
[cos_a, sin_a, 0],
[-sin_a, cos_a, 0],
[0, 0, 1],
]
quat = quaternion_from_angle_axis(angle, axis)
2020-06-01 16:21:18 -07:00
return rotation_matrix_transpose_from_quaternion(quat)
def rotation_matrix(angle, axis):
"""
Rotation in R^3 about a specified axis of rotation.
"""
return np.transpose(rotation_matrix_transpose(angle, axis))
def rotation_about_z(angle):
return [
2020-02-21 10:57:10 -08:00
[math.cos(angle), -math.sin(angle), 0],
[math.sin(angle), math.cos(angle), 0],
[0, 0, 1]
]
def z_to_vector(vector):
"""
Returns some matrix in SO(3) which takes the z-axis to the
(normalized) vector provided as an argument
"""
2018-08-15 17:30:24 -07:00
norm = get_norm(vector)
if norm == 0:
return np.identity(3)
v = np.array(vector) / norm
phi = np.arccos(v[2])
if any(v[:2]):
# projection of vector to unit circle
2018-08-15 17:30:24 -07:00
axis_proj = v[:2] / get_norm(v[:2])
theta = np.arccos(axis_proj[0])
if axis_proj[1] < 0:
theta = -theta
else:
theta = 0
phi_down = np.array([
2020-02-21 10:57:10 -08:00
[math.cos(phi), 0, math.sin(phi)],
[0, 1, 0],
2020-02-21 10:57:10 -08:00
[-math.sin(phi), 0, math.cos(phi)]
])
return np.dot(rotation_about_z(theta), phi_down)
def angle_of_vector(vector):
"""
Returns polar coordinate theta when vector is project on xy plane
"""
return np.angle(complex(*vector[:2]))
def angle_between_vectors(v1, v2):
"""
Returns the angle between two 3D vectors.
This angle will always be btw 0 and pi
"""
2020-02-11 19:48:50 -08:00
diff = (angle_of_vector(v2) - angle_of_vector(v1)) % TAU
return min(diff, TAU - diff)
def project_along_vector(point, vector):
matrix = np.identity(3) - np.outer(vector, vector)
return np.dot(point, matrix.T)
def normalize(vect, fall_back=None):
norm = get_norm(vect)
if norm > 0:
2018-08-30 14:24:57 -07:00
return np.array(vect) / norm
2020-02-21 10:57:10 -08:00
elif fall_back is not None:
return fall_back
else:
2020-02-21 10:57:10 -08:00
return np.zeros(len(vect))
def cross(v1, v2):
return np.array([
v1[1] * v2[2] - v1[2] * v2[1],
v1[2] * v2[0] - v1[0] * v2[2],
v1[0] * v2[1] - v1[1] * v2[0]
])
def get_unit_normal(v1, v2):
return normalize(cross(v1, v2))
###
def compass_directions(n=4, start_vect=RIGHT):
2018-10-05 17:19:48 -07:00
angle = TAU / n
return np.array([
rotate_vector(start_vect, k * angle)
for k in range(n)
])
def complex_to_R3(complex_num):
return np.array((complex_num.real, complex_num.imag, 0))
def R3_to_complex(point):
return complex(*point[:2])
2018-05-30 12:02:25 -07:00
def complex_func_to_R3_func(complex_func):
return lambda p: complex_to_R3(complex_func(R3_to_complex(p)))
def center_of_mass(points):
points = [np.array(point).astype("float") for point in points]
return sum(points) / len(points)
2018-07-14 10:32:13 -07:00
2019-05-27 19:48:33 -07:00
def midpoint(point1, point2):
return center_of_mass([point1, point2])
2018-07-14 10:32:13 -07:00
def line_intersection(line1, line2):
"""
return intersection point of two lines,
each defined with a pair of vectors determining
the end points
"""
x_diff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
y_diff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(x_diff, y_diff)
if div == 0:
raise Exception("Lines do not intersect")
d = (det(*line1), det(*line2))
x = det(d, x_diff) / div
y = det(d, y_diff) / div
return np.array([x, y, 0])
2018-08-28 09:45:12 -07:00
2020-02-04 15:24:16 -08:00
def find_intersection(p0, v0, p1, v1, threshold=1e-5):
"""
Return the intersection of a line passing through p0 in direction v0
with one passing through p1 in direction v1. (Or array of intersections
from arrays of such points/directions).
For 3d values, it returns the point on the ray p0 + v0 * t closest to the
ray p1 + v1 * t
"""
p0 = np.array(p0, ndmin=2)
v0 = np.array(v0, ndmin=2)
p1 = np.array(p1, ndmin=2)
v1 = np.array(v1, ndmin=2)
m, n = np.shape(p0)
assert(n in [2, 3])
numer = np.cross(v1, p1 - p0)
denom = np.cross(v1, v0)
if n == 3:
d = len(np.shape(numer))
new_numer = np.multiply(numer, numer).sum(d - 1)
new_denom = np.multiply(denom, numer).sum(d - 1)
numer, denom = new_numer, new_denom
denom[abs(denom) < threshold] = np.inf # So that ratio goes to 0 there
ratio = numer / denom
ratio = np.repeat(ratio, n).reshape((m, n))
return p0 + ratio * v0
2018-08-28 09:45:12 -07:00
def get_winding_number(points):
total_angle = 0
for p1, p2 in adjacent_pairs(points):
d_angle = angle_of_vector(p2) - angle_of_vector(p1)
d_angle = ((d_angle + PI) % TAU) - PI
total_angle += d_angle
return total_angle / TAU
##
def cross2d(a, b):
if len(a.shape) == 2:
return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
else:
return a[0] * b[1] - b[0] * a[1]
def tri_area(a, b, c):
return 0.5 * abs(
a[0] * (b[1] - c[1]) +
b[0] * (c[1] - a[1]) +
c[0] * (a[1] - b[1])
)
def is_inside_triangle(p, a, b, c):
"""
Test if point p is inside triangle abc
"""
crosses = np.array([
cross2d(p - a, b - p),
cross2d(p - b, c - p),
cross2d(p - c, a - p),
])
return np.all(crosses > 0) or np.all(crosses < 0)
def norm_squared(v):
return sum(v * v)
2020-02-11 19:48:50 -08:00
# TODO, fails for polygons drawn over themselves
def earclip_triangulation(verts, rings):
n = len(verts)
# Establish where loop indices should be connected
loop_connections = dict()
for e0, e1 in zip(rings, rings[1:]):
temp_i = e0
2020-02-20 16:50:34 -08:00
# Find closet point in the first ring (j) to
# the first index of this ring (i)
norms = np.array([
[j, norm_squared(verts[temp_i] - verts[j])]
2020-02-20 16:50:34 -08:00
for j in range(0, rings[0])
if j not in loop_connections
])
2020-02-20 16:50:34 -08:00
j = int(norms[norms[:, 1].argmin()][0])
# Find i closest to this j
norms = np.array([
[i, norm_squared(verts[i] - verts[j])]
for i in range(e0, e1)
2020-02-20 16:50:34 -08:00
if i not in loop_connections
])
2020-02-20 16:50:34 -08:00
i = int(norms[norms[:, 1].argmin()][0])
loop_connections[i] = j
loop_connections[j] = i
# Setup linked list
after = []
e0 = 0
for e1 in rings:
after.extend([*range(e0 + 1, e1), e0])
e0 = e1
# Find an ordering of indices walking around the polygon
indices = []
i = 0
2020-02-11 19:48:50 -08:00
for x in range(n + len(rings) - 1):
# starting = False
if i in loop_connections:
j = loop_connections[i]
indices.extend([i, j])
i = after[j]
else:
indices.append(i)
i = after[i]
2020-02-11 19:48:50 -08:00
if i == 0:
break
meta_indices = earcut(verts[indices, :2], [len(indices)])
return [indices[mi] for mi in meta_indices]
def old_earclip_triangulation(verts, rings, orientation):
n = len(verts)
assert(n in rings)
result = []
# Establish where loop indices should be connected
loop_connections = dict()
e0 = 0
for e1 in rings:
norms = np.array([
[i, j, get_norm(verts[i] - verts[j])]
for i in range(e0, e1)
for j in it.chain(range(0, e0), range(e1, n))
])
if len(norms) == 0:
continue
i, j = norms[np.argmin(norms[:, 2])][:2].astype(int)
loop_connections[i] = j
loop_connections[j] = i
e0 = e1
# Setup bidirectional linked list
before = []
after = []
e0 = 0
for e1 in rings:
after += [*range(e0 + 1, e1), e0]
before += [e1 - 1, *range(e0, e1 - 1)]
e0 = e1
# Initialize edge triangles
edge_tris = []
i = 0
starting = True
while (i != 0 or starting):
starting = False
if i in loop_connections:
j = loop_connections[i]
edge_tris.append([before[i], i, j])
edge_tris.append([i, j, after[j]])
i = after[j]
else:
edge_tris.append([before[i], i, after[i]])
i = after[i]
# Set up a test for whether or not three indices
# form an ear of the polygon, meaning a convex corner
# which doesn't contain any other vertices
indices = list(range(n))
def is_ear(*tri_indices):
tri = [verts[i] for i in tri_indices]
v1 = tri[1] - tri[0]
v2 = tri[2] - tri[1]
cross = v1[0] * v2[1] - v2[0] * v1[1]
if orientation * cross < 0:
return False
for j in indices:
if j in tri_indices:
continue
elif is_inside_triangle(verts[j], *tri):
return False
return True
# Loop through and clip off all the ears
n_failures = 0
i = 0
while n_failures < len(edge_tris):
n = len(edge_tris)
edge_tri = edge_tris[i % n]
if is_ear(*edge_tri):
result.extend(edge_tri)
edge_tris[(i - 1) % n][2] = edge_tri[2]
edge_tris[(i + 1) % n][0] = edge_tri[0]
if edge_tri[1] in indices:
indices.remove(edge_tri[1])
edge_tris.remove(edge_tri)
n_failures = 0
else:
n_failures += 1
i += 1
return result