mirror of
https://github.com/3b1b/manim.git
synced 2025-07-28 20:43:56 +08:00
Fix triangulation
This commit is contained in:
@ -1,7 +1,5 @@
|
|||||||
import itertools as it
|
import itertools as it
|
||||||
import sys
|
import moderngl
|
||||||
from mapbox_earcut import triangulate_float32 as earcut
|
|
||||||
import numbers
|
|
||||||
|
|
||||||
from colour import Color
|
from colour import Color
|
||||||
|
|
||||||
@ -20,7 +18,10 @@ from manimlib.utils.iterables import make_even
|
|||||||
from manimlib.utils.iterables import stretch_array_to_length
|
from manimlib.utils.iterables import stretch_array_to_length
|
||||||
from manimlib.utils.iterables import stretch_array_to_length_with_interpolation
|
from manimlib.utils.iterables import stretch_array_to_length_with_interpolation
|
||||||
from manimlib.utils.iterables import listify
|
from manimlib.utils.iterables import listify
|
||||||
|
from manimlib.utils.space_ops import cross2d
|
||||||
from manimlib.utils.space_ops import get_norm
|
from manimlib.utils.space_ops import get_norm
|
||||||
|
from manimlib.utils.space_ops import angle_between_vectors
|
||||||
|
from manimlib.utils.space_ops import earclip_triangulation
|
||||||
|
|
||||||
|
|
||||||
class VMobject(Mobject):
|
class VMobject(Mobject):
|
||||||
@ -54,6 +55,7 @@ class VMobject(Mobject):
|
|||||||
"fill_frag_shader_file": "quadratic_bezier_fill_frag.glsl",
|
"fill_frag_shader_file": "quadratic_bezier_fill_frag.glsl",
|
||||||
# Could also be Bevel, Miter, Round
|
# Could also be Bevel, Miter, Round
|
||||||
"joint_type": "auto",
|
"joint_type": "auto",
|
||||||
|
"render_primative": moderngl.TRIANGLES,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_group_class(self):
|
def get_group_class(self):
|
||||||
@ -331,7 +333,7 @@ class VMobject(Mobject):
|
|||||||
submob.z_index_group = self
|
submob.z_index_group = self
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def stretch_style_for_points(self, array):
|
def stretched_style_array_matching_points(self, array):
|
||||||
new_len = self.get_num_points()
|
new_len = self.get_num_points()
|
||||||
long_arr = stretch_array_to_length_with_interpolation(
|
long_arr = stretch_array_to_length_with_interpolation(
|
||||||
array, 1 + 2 * (new_len // 3)
|
array, 1 + 2 * (new_len // 3)
|
||||||
@ -371,11 +373,11 @@ class VMobject(Mobject):
|
|||||||
# TODO, check that number new points is a multiple of 4?
|
# TODO, check that number new points is a multiple of 4?
|
||||||
# or else that if len(self.points) % 4 == 1, then
|
# or else that if len(self.points) % 4 == 1, then
|
||||||
# len(new_points) % 4 == 3?
|
# len(new_points) % 4 == 3?
|
||||||
self.points = np.append(self.points, new_points, axis=0)
|
self.points = np.vstack([self.points, new_points])
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def start_new_path(self, point):
|
def start_new_path(self, point):
|
||||||
# TODO, make sure that len(self.points) % 3 == 0?
|
assert(len(self.points) % self.n_points_per_curve == 0)
|
||||||
self.append_points([point])
|
self.append_points([point])
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -437,11 +439,37 @@ class VMobject(Mobject):
|
|||||||
def get_reflection_of_last_handle(self):
|
def get_reflection_of_last_handle(self):
|
||||||
return 2 * self.points[-1] - self.points[-2]
|
return 2 * self.points[-1] - self.points[-2]
|
||||||
|
|
||||||
|
def close_path(self):
|
||||||
|
if not self.is_closed():
|
||||||
|
self.add_line_to(self.get_subpaths()[-1][0])
|
||||||
|
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self.consider_points_equals(
|
return self.consider_points_equals(
|
||||||
self.points[0], self.points[-1]
|
self.points[0], self.points[-1]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def subdivide_sharp_curves(self, angle_threshold=30 * DEGREES, family=True):
|
||||||
|
if family:
|
||||||
|
vmobs = self.family_members_with_points()
|
||||||
|
else:
|
||||||
|
vmobs = [self] if self.has_points() else []
|
||||||
|
|
||||||
|
for vmob in vmobs:
|
||||||
|
new_points = []
|
||||||
|
for tup in vmob.get_bezier_tuples():
|
||||||
|
angle = angle_between_vectors(tup[1] - tup[0], tup[2] - tup[1])
|
||||||
|
if angle > angle_threshold:
|
||||||
|
n = int(np.ceil(angle / angle_threshold))
|
||||||
|
alphas = np.linspace(0, 1, n + 1)
|
||||||
|
new_points.extend([
|
||||||
|
partial_bezier_points(tup, a1, a2)
|
||||||
|
for a1, a2 in zip(alphas, alphas[1:])
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
new_points.append(tup)
|
||||||
|
vmob.points = np.vstack(new_points)
|
||||||
|
return self
|
||||||
|
|
||||||
def add_points_as_corners(self, points):
|
def add_points_as_corners(self, points):
|
||||||
for point in points:
|
for point in points:
|
||||||
self.add_line_to(point)
|
self.add_line_to(point)
|
||||||
@ -468,11 +496,7 @@ class VMobject(Mobject):
|
|||||||
subpaths = submob.get_subpaths()
|
subpaths = submob.get_subpaths()
|
||||||
submob.clear_points()
|
submob.clear_points()
|
||||||
for subpath in subpaths:
|
for subpath in subpaths:
|
||||||
anchors = np.append(
|
anchors = np.vstack([subpath[::nppc], subpath[-1:]])
|
||||||
subpath[::nppc],
|
|
||||||
subpath[-1:],
|
|
||||||
0
|
|
||||||
)
|
|
||||||
if mode == "smooth":
|
if mode == "smooth":
|
||||||
h1, h2 = get_smooth_handle_points(anchors)
|
h1, h2 = get_smooth_handle_points(anchors)
|
||||||
new_subpath = get_quadratic_approximation_of_cubic(
|
new_subpath = get_quadratic_approximation_of_cubic(
|
||||||
@ -519,7 +543,7 @@ class VMobject(Mobject):
|
|||||||
atol=self.tolerance_for_point_equality
|
atol=self.tolerance_for_point_equality
|
||||||
)
|
)
|
||||||
|
|
||||||
# Information about line
|
# Information about the curve
|
||||||
def get_bezier_tuples_from_points(self, points):
|
def get_bezier_tuples_from_points(self, points):
|
||||||
nppc = self.n_points_per_curve
|
nppc = self.n_points_per_curve
|
||||||
remainder = len(points) % nppc
|
remainder = len(points) % nppc
|
||||||
@ -643,8 +667,8 @@ class VMobject(Mobject):
|
|||||||
diff2 = max(0, (len(sp1) - len(sp2)) // nppc)
|
diff2 = max(0, (len(sp1) - len(sp2)) // nppc)
|
||||||
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
|
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
|
||||||
sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
|
sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
|
||||||
new_path1 = np.append(new_path1, sp1, axis=0)
|
new_path1 = np.vstack([new_path1, sp1])
|
||||||
new_path2 = np.append(new_path2, sp2, axis=0)
|
new_path2 = np.vstack([new_path2, sp2])
|
||||||
self.set_points(new_path1)
|
self.set_points(new_path1)
|
||||||
vmobject.set_points(new_path2)
|
vmobject.set_points(new_path2)
|
||||||
return self
|
return self
|
||||||
@ -690,11 +714,10 @@ class VMobject(Mobject):
|
|||||||
# smaller quadratic curves
|
# smaller quadratic curves
|
||||||
alphas = np.linspace(0, 1, sf + 1)
|
alphas = np.linspace(0, 1, sf + 1)
|
||||||
for a1, a2 in zip(alphas, alphas[1:]):
|
for a1, a2 in zip(alphas, alphas[1:]):
|
||||||
new_points = np.append(
|
new_points = np.vstack([
|
||||||
new_points,
|
new_points,
|
||||||
partial_bezier_points(group, a1, a2),
|
partial_bezier_points(group, a1, a2),
|
||||||
axis=0
|
])
|
||||||
)
|
|
||||||
return new_points
|
return new_points
|
||||||
|
|
||||||
def align_rgbas(self, vmobject):
|
def align_rgbas(self, vmobject):
|
||||||
@ -782,6 +805,7 @@ class VMobject(Mobject):
|
|||||||
"vert": self.fill_vert_shader_file,
|
"vert": self.fill_vert_shader_file,
|
||||||
"geom": self.fill_geom_shader_file,
|
"geom": self.fill_geom_shader_file,
|
||||||
"frag": self.fill_frag_shader_file,
|
"frag": self.fill_frag_shader_file,
|
||||||
|
"render_primative": self.render_primative,
|
||||||
})
|
})
|
||||||
if self.get_stroke_width() > 0 and self.get_stroke_opacity() > 0:
|
if self.get_stroke_width() > 0 and self.get_stroke_opacity() > 0:
|
||||||
result.append({
|
result.append({
|
||||||
@ -789,6 +813,7 @@ class VMobject(Mobject):
|
|||||||
"vert": self.stroke_vert_shader_file,
|
"vert": self.stroke_vert_shader_file,
|
||||||
"geom": self.stroke_geom_shader_file,
|
"geom": self.stroke_geom_shader_file,
|
||||||
"frag": self.stroke_frag_shader_file,
|
"frag": self.stroke_frag_shader_file,
|
||||||
|
"render_primative": self.render_primative,
|
||||||
})
|
})
|
||||||
if len(result) == 2 and self.draw_stroke_behind_fill:
|
if len(result) == 2 and self.draw_stroke_behind_fill:
|
||||||
return [result[1], result[0]]
|
return [result[1], result[0]]
|
||||||
@ -813,11 +838,11 @@ class VMobject(Mobject):
|
|||||||
|
|
||||||
rgbas = self.get_stroke_rgbas()
|
rgbas = self.get_stroke_rgbas()
|
||||||
if len(rgbas) > 1:
|
if len(rgbas) > 1:
|
||||||
rgbas = self.stretch_style_for_points(rgbas)
|
rgbas = self.stretched_style_array_matching_points(rgbas)
|
||||||
|
|
||||||
stroke_width = self.stroke_width
|
stroke_width = self.stroke_width
|
||||||
if len(stroke_width) > 1:
|
if len(stroke_width) > 1:
|
||||||
stroke_width = self.stretch_style_for_points(stroke_width)
|
stroke_width = self.stretched_style_array_matching_points(stroke_width)
|
||||||
|
|
||||||
data = np.zeros(len(points), dtype=dtype)
|
data = np.zeros(len(points), dtype=dtype)
|
||||||
data['point'] = points
|
data['point'] = points
|
||||||
@ -831,12 +856,10 @@ class VMobject(Mobject):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def get_triangulation(self):
|
def get_triangulation(self):
|
||||||
# Figure out how to triangulate the interior of the vmob,
|
# Figure out how to triangulate the interior to know
|
||||||
# and pass the appropriate attributes to each triangle vertex
|
# how to send the points as to the vertex shader.
|
||||||
# First triangles come directly from the points
|
# First triangles come directly from the points
|
||||||
|
|
||||||
# TODO, this does not work for compound paths that aren't inside each other
|
|
||||||
|
|
||||||
points = self.points
|
points = self.points
|
||||||
indices = np.arange(len(points), dtype=int)
|
indices = np.arange(len(points), dtype=int)
|
||||||
|
|
||||||
@ -846,98 +869,68 @@ class VMobject(Mobject):
|
|||||||
v01s = b1s - b0s
|
v01s = b1s - b0s
|
||||||
v12s = b2s - b1s
|
v12s = b2s - b1s
|
||||||
|
|
||||||
def cross(a, b):
|
|
||||||
return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
|
|
||||||
|
|
||||||
# TODO, account fo 3d
|
# TODO, account fo 3d
|
||||||
crosses = cross(v01s, v12s)
|
crosses = cross2d(v01s, v12s)
|
||||||
orientations = np.ones(crosses.size)
|
orientations = np.sign(crosses)
|
||||||
orientations[crosses <= 0] = -1
|
|
||||||
|
|
||||||
atol = self.tolerance_for_point_equality
|
atol = self.tolerance_for_point_equality
|
||||||
end_of_loop = np.zeros(orientations.shape, dtype=bool)
|
end_of_loop = np.zeros(orientations.shape, dtype=bool)
|
||||||
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
|
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
|
||||||
end_of_loop[-1] = True
|
end_of_loop[-1] = True
|
||||||
end_of_loop_indices = np.argwhere(end_of_loop).flatten()
|
|
||||||
|
|
||||||
# Add up (x1 + x2)*(y1 - y2) for all edges (x1, y1), (x2, y2)
|
# Add up (x1 + x2)*(y2 - y1) for all edges (x1, y1), (x2, y2)
|
||||||
signed_area_terms = (b0s[:, 0] - b2s[:, 0]) * (b0s[:, 1] + b2s[:, 1])
|
signed_area_terms = (b0s[:, 0] + b2s[:, 0]) * (b2s[:, 1] - b0s[:, 1])
|
||||||
|
|
||||||
loop_orientations = np.array([
|
|
||||||
signed_area_terms[i:j].sum()
|
|
||||||
for i, j in zip([0, *end_of_loop_indices], end_of_loop_indices)
|
|
||||||
])
|
|
||||||
# Total signed area determines orientation
|
# Total signed area determines orientation
|
||||||
total_orientation = np.sign(loop_orientations.sum())
|
total_orientation = np.sign(signed_area_terms.sum())
|
||||||
orientations *= total_orientation
|
orientations *= total_orientation
|
||||||
loop_orientations *= total_orientation
|
|
||||||
concave_parts = orientations < 0
|
concave_parts = orientations < 0
|
||||||
|
|
||||||
# These are the vertices to which we'll apply a polygon triangulation
|
# These are the vertices to which we'll apply a polygon triangulation
|
||||||
inner_vert_indices = np.array([
|
inner_vert_indices = np.hstack([
|
||||||
*indices[0::3],
|
indices[0::3],
|
||||||
*indices[1::3][concave_parts],
|
indices[1::3][concave_parts],
|
||||||
*indices[2::3][end_of_loop],
|
indices[2::3][end_of_loop],
|
||||||
])
|
])
|
||||||
inner_vert_indices.sort()
|
inner_vert_indices.sort()
|
||||||
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
|
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
|
||||||
|
|
||||||
# Triangulate
|
# Triangulate
|
||||||
# Group together each positive loop with all the negatives following it
|
inner_verts = points[inner_vert_indices]
|
||||||
inner_verts = points[inner_vert_indices, :2]
|
inner_tri_indices = inner_vert_indices[
|
||||||
# inner_tri_indices = []
|
earclip_triangulation(inner_verts, rings, total_orientation)
|
||||||
# positive_loops = indices[:len(rings)][loop_orientations > 0]
|
]
|
||||||
# last_end = 0
|
|
||||||
# for i, j in zip(positive_loops, [*positive_loops[1:], len(rings)]):
|
|
||||||
# print(i, j, rings, last_end)
|
|
||||||
# triangulation = earcut(inner_verts[last_end:rings[j - 1]], rings[i:j] - last_end)
|
|
||||||
# new_tri_indices = inner_vert_indices[triangulation]
|
|
||||||
# inner_tri_indices += list(new_tri_indices)
|
|
||||||
# last_end = rings[j - 1]
|
|
||||||
|
|
||||||
inner_tri_indices = inner_vert_indices[earcut(inner_verts, rings)]
|
tri_indices = np.hstack([indices, inner_tri_indices])
|
||||||
|
return tri_indices, total_orientation
|
||||||
# This is faster than using np.append
|
|
||||||
tri_indices = np.zeros(len(indices) + len(inner_tri_indices), dtype=int)
|
|
||||||
tri_indices[:len(indices)] = indices
|
|
||||||
tri_indices[len(indices):] = inner_tri_indices
|
|
||||||
|
|
||||||
fill_type_to_code = {
|
|
||||||
"inside": 0,
|
|
||||||
"outside": 1,
|
|
||||||
"all": 2,
|
|
||||||
}
|
|
||||||
fill_types = np.ones((len(tri_indices), 1))
|
|
||||||
fill_types[:len(points)] = fill_type_to_code["inside"]
|
|
||||||
fill_types[:len(points)][np.repeat(concave_parts, 3)] = fill_type_to_code["outside"]
|
|
||||||
fill_types[len(points):] = fill_type_to_code["all"]
|
|
||||||
|
|
||||||
return tri_indices, fill_types
|
|
||||||
|
|
||||||
def get_fill_shader_data(self):
|
def get_fill_shader_data(self):
|
||||||
dtype = [
|
dtype = [
|
||||||
('point', np.float32, (3,)),
|
('point', np.float32, (3,)),
|
||||||
('color', np.float32, (4,)),
|
('color', np.float32, (4,)),
|
||||||
('fill_type', np.float32, (1,)),
|
('fill_all', np.float32, (1,)),
|
||||||
|
('orientation', np.float32, (1,)),
|
||||||
]
|
]
|
||||||
|
|
||||||
points = self.points
|
points = self.points
|
||||||
|
|
||||||
# TODO, potentially cache triangulation
|
# TODO, potentially cache triangulation
|
||||||
tri_indices, fill_types = self.get_triangulation()
|
tri_indices, orientation = self.get_triangulation()
|
||||||
|
|
||||||
rgbas = self.get_fill_rgbas() # TODO, best way to enable multiple colors?
|
rgbas = self.get_fill_rgbas() # TODO, best way to enable multiple colors?
|
||||||
# rgbas = self.stretch_style_for_points(rgbas)
|
|
||||||
# rgbas = rgbas[tri_indices]
|
|
||||||
# rgbas = np.random.random(data["color"].shape)
|
|
||||||
|
|
||||||
data = np.zeros(len(tri_indices), dtype=dtype)
|
data = np.zeros(len(tri_indices), dtype=dtype)
|
||||||
data["point"] = points[tri_indices]
|
data["point"] = points[tri_indices]
|
||||||
data["color"] = rgbas
|
data["color"] = rgbas
|
||||||
data["fill_type"] = fill_types
|
# Assume the triangulation is such that the first n_points points
|
||||||
|
# are on the boundary, and the rest are in the interior
|
||||||
|
data["fill_all"][:len(points)] = 0
|
||||||
|
data["fill_all"][len(points):] = 1
|
||||||
|
data["orientation"] = orientation
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class VGroup(VMobject):
|
class VGroup(VMobject):
|
||||||
def __init__(self, *vmobjects, **kwargs):
|
def __init__(self, *vmobjects, **kwargs):
|
||||||
if not all([isinstance(m, VMobject) for m in vmobjects]):
|
if not all([isinstance(m, VMobject) for m in vmobjects]):
|
||||||
|
Reference in New Issue
Block a user