Merge pull request #1959 from 3b1b/video-work

Simplify quadratic bezier shaders
This commit is contained in:
Grant Sanderson 2023-01-15 09:39:16 -08:00 committed by GitHub
commit de38b56d0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1125 additions and 1254 deletions

View file

@ -486,7 +486,7 @@ class GraphExample(Scene):
self.wait()
class TexAndNumbersExample(InteractiveScene):
class TexAndNumbersExample(Scene):
def construct(self):
axes = Axes((-3, 3), (-3, 3), unit_size=1)
axes.to_edge(DOWN)

View file

@ -65,6 +65,7 @@ class Animation(object):
self.rate_func = squish_rate_func(
self.rate_func, start / self.run_time, end / self.run_time,
)
self.mobject.refresh_shader_data()
self.mobject.set_animating_status(True)
self.starting_mobject = self.create_starting_mobject()
if self.suspend_mobject_updating:

View file

@ -198,32 +198,33 @@ class VShowPassingFlash(Animation):
self,
vmobject: VMobject,
time_width: float = 0.3,
taper_width: float = 0.02,
taper_width: float = 0.05,
remover: bool = True,
**kwargs
):
self.time_width = time_width
self.taper_width = taper_width
super().__init__(vmobject, remover=remover, **kwargs)
self.mobject = vmobject
def taper_kernel(self, x):
if x < self.taper_width:
return x
elif x > 1 - self.taper_width:
return 1.0 - x
return 1.0
def begin(self) -> None:
self.mobject.align_stroke_width_data_to_points()
# Compute an array of stroke widths for each submobject
# which tapers out at either end
self.submob_to_anchor_widths = dict()
self.submob_to_widths = dict()
for sm in self.mobject.get_family():
original_widths = sm.get_stroke_widths()
anchor_widths = np.array([*original_widths[0::3], original_widths[-1]])
def taper_kernel(x):
if x < self.taper_width:
return x
elif x > 1 - self.taper_width:
return 1.0 - x
return 1.0
taper_array = list(map(taper_kernel, np.linspace(0, 1, len(anchor_widths))))
self.submob_to_anchor_widths[hash(sm)] = anchor_widths * taper_array
widths = sm.get_stroke_widths()
self.submob_to_widths[hash(sm)] = np.array([
width * self.taper_kernel(x)
for width, x in zip(widths, np.linspace(0, 1, len(widths)))
])
super().begin()
def interpolate_submobject(
@ -232,26 +233,20 @@ class VShowPassingFlash(Animation):
starting_sumobject: None,
alpha: float
) -> None:
anchor_widths = self.submob_to_anchor_widths[hash(submobject)]
widths = self.submob_to_widths[hash(submobject)]
# Create a gaussian such that 3 sigmas out on either side
# will equals time_width
tw = self.time_width
sigma = tw / 6
mu = interpolate(-tw / 2, 1 + tw / 2, alpha)
xs = np.linspace(0, 1, len(widths))
zs = (xs - mu) / sigma
gaussian = np.exp(-0.5 * zs * zs)
gaussian[abs(xs - mu) > 3 * sigma] = 0
def gauss_kernel(x):
if abs(x - mu) > 3 * sigma:
return 0
z = (x - mu) / sigma
return math.exp(-0.5 * z * z)
submobject.set_stroke(width=widths * gaussian)
kernel_array = list(map(gauss_kernel, np.linspace(0, 1, len(anchor_widths))))
scaled_widths = anchor_widths * kernel_array
new_widths = np.zeros(submobject.get_num_points())
new_widths[0::3] = scaled_widths[:-1]
new_widths[2::3] = scaled_widths[1:]
new_widths[1::3] = (new_widths[0::3] + new_widths[2::3]) / 2
submobject.set_stroke(width=new_widths)
def finish(self) -> None:
super().finish()

View file

@ -40,8 +40,12 @@ class Rotating(Animation):
)
def interpolate_mobject(self, alpha: float) -> None:
for sm1, sm2 in self.get_all_families_zipped():
sm1.set_points(sm2.get_points())
pairs = zip(
self.mobject.family_members_with_points(),
self.starting_mobject.family_members_with_points(),
)
for sm1, sm2 in pairs:
sm1.data["points"][:] = sm2.data["points"]
self.mobject.rotate(
self.rate_func(alpha) * self.angle,
axis=self.axis,

View file

@ -42,8 +42,8 @@ class TransformMatchingParts(AnimationGroup):
self.anim_config = dict(**kwargs)
# We will progressively build up a list of transforms
# from characters in source to those in target. These
# two lists keep track of which characters are accounted
# from pieces in source to those in target. These
# two lists keep track of which pieces are accounted
# for so far
self.source_pieces = source.family_members_with_points()
self.target_pieces = target.family_members_with_points()
@ -57,14 +57,18 @@ class TransformMatchingParts(AnimationGroup):
self.add_transform(*pair)
# Finally, account for mismatches
for source_char in self.source_pieces:
for source_piece in self.source_pieces:
if any([source_piece in anim.mobject.get_family() for anim in self.anims]):
continue
self.anims.append(FadeOutToPoint(
source_char, target.get_center(),
source_piece, target.get_center(),
**self.anim_config
))
for target_char in self.target_pieces:
for target_piece in self.target_pieces:
if any([target_piece in anim.mobject.get_family() for anim in self.anims]):
continue
self.anims.append(FadeInFromPoint(
target_char, source.get_center(),
target_piece, source.get_center(),
**self.anim_config
))

View file

@ -250,6 +250,12 @@ class Camera(object):
else:
self.ctx.disable(moderngl.DEPTH_TEST)
def set_ctx_clip_distance(self, enable: bool = True) -> None:
if enable:
gl.glEnable(gl.GL_CLIP_DISTANCE0)
else:
gl.glDisable(gl.GL_CLIP_DISTANCE0)
def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position)
@ -377,6 +383,7 @@ class Camera(object):
shader_program = render_group["prog"]
self.set_shader_uniforms(shader_program, shader_wrapper)
self.set_ctx_depth_test(shader_wrapper.depth_test)
self.set_ctx_clip_distance(shader_wrapper.use_clip_plane)
render_group["vao"].render(int(shader_wrapper.render_primitive))
if render_group["single_use"]:
self.release_render_group(render_group)
@ -402,16 +409,23 @@ class Camera(object):
shader_wrapper: ShaderWrapper,
single_use: bool = True
) -> dict[str, Any]:
# Data buffers
vbo = self.ctx.buffer(shader_wrapper.vert_data.tobytes())
if shader_wrapper.vert_indices is None:
# Data buffer
vert_data = shader_wrapper.vert_data
indices = shader_wrapper.vert_indices
if indices is None:
ibo = None
elif single_use:
ibo = self.ctx.buffer(indices)
else:
vert_index_data = shader_wrapper.vert_indices.astype('i4').tobytes()
if vert_index_data:
ibo = self.ctx.buffer(vert_index_data)
else:
ibo = None
# The vao.render call is strangely longer
# when an index buffer is used, so if the
# mobject is not changing, meaning only its
# uniforms are being updated, just create
# a larger data array based on the indices
# and don't bother with the ibo
vert_data = vert_data[indices]
ibo = None
vbo = self.ctx.buffer(vert_data)
# Program and vertex array
shader_program, vert_format = self.get_shader_program(shader_wrapper)

View file

@ -65,8 +65,8 @@ RADIANS: float = 1
FFMPEG_BIN: str = "ffmpeg"
JOINT_TYPE_MAP: dict = {
"auto": 0,
"round": 1,
"no_joint": 0,
"auto": 1,
"bevel": 2,
"miter": 3,
}

View file

@ -60,18 +60,17 @@ def get_scene_config(config):
for key in set(scene_parameters).intersection(config.keys())
}
def compute_total_frames(scene_class, scene_config):
"""
When a scene is being written to file, a copy of the scene is run with
skip_animations set to true so as to count how many frames it will require.
This allows for a total progress bar on rendering, and also allows runtime
errors to be exposed preemptively for long running scenes. The final frame
is saved by default, so that one can more quickly check that the last frame
looks as expected.
errors to be exposed preemptively for long running scenes.
"""
pre_config = copy.deepcopy(scene_config)
pre_config["file_writer_config"]["write_to_movie"] = False
pre_config["file_writer_config"]["save_last_frame"] = True
pre_config["file_writer_config"]["save_last_frame"] = False
pre_config["file_writer_config"]["quiet"] = True
pre_config["skip_animations"] = True
pre_scene = scene_class(**pre_config)

View file

@ -18,7 +18,7 @@ def _convert_vmobject_to_skia_path(vmobject: VMobject) -> pathops.Path:
path.moveTo(*start[:2])
for p0, p1, p2 in quads:
path.quadTo(*p1[:2], *p2[:2])
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
if vmobject.consider_points_equal(subpath[0], subpath[-1]):
path.close()
return path

View file

@ -92,10 +92,11 @@ class ImplicitFunction(VMobject):
y_range: Tuple[float, float] = (-FRAME_Y_RADIUS, FRAME_Y_RADIUS),
min_depth: int = 5,
max_quads: int = 1500,
use_smoothing: bool = True,
use_smoothing: bool = False,
joint_type: str = 'no_joint',
**kwargs
):
super().__init__(**kwargs)
super().__init__(joint_type=joint_type, **kwargs)
p_min, p_max = (
np.array([x_range[0], y_range[0]]),
@ -109,7 +110,9 @@ class ImplicitFunction(VMobject):
max_quads=max_quads,
) # returns a list of lists of 2D points
curves = [
np.pad(curve, [(0, 0), (0, 1)]) for curve in curves if curve != []
np.pad(curve, [(0, 0), (0, 1)])
for curve in curves
if curve != []
] # add z coord as 0
for curve in curves:
self.start_new_path(curve[0])

View file

@ -19,6 +19,7 @@ from manimlib.utils.simple_functions import clip
from manimlib.utils.simple_functions import fdiv
from manimlib.utils.space_ops import angle_between_vectors
from manimlib.utils.space_ops import angle_of_vector
from manimlib.utils.space_ops import cross2d
from manimlib.utils.space_ops import compass_directions
from manimlib.utils.space_ops import find_intersection
from manimlib.utils.space_ops import get_norm
@ -225,22 +226,13 @@ class Arc(TipableVMobject):
angle: float,
start_angle: float = 0,
n_components: int = 8
) -> Vect3:
samples = np.array([
[np.cos(a), np.sin(a), 0]
for a in np.linspace(
start_angle,
start_angle + angle,
2 * n_components + 1,
)
])
) -> Vect3Array:
n_points = 2 * n_components + 1
angles = np.linspace(start_angle, start_angle + angle, n_points)
points = np.array([np.cos(angles), np.sin(angles), np.zeros(n_points)]).T
# Adjust handles
theta = angle / n_components
samples[1::2] /= np.cos(theta / 2)
points = np.zeros((3 * n_components, 3))
points[0::3] = samples[0:-1:2]
points[1::3] = samples[1::2]
points[2::3] = samples[2::2]
points[1::2] /= np.cos(theta / 2)
return points
def get_arc_center(self) -> Vect3:
@ -416,9 +408,9 @@ class AnnularSector(VMobject):
)
for radius in (inner_radius, outer_radius)
]
self.append_points(inner_arc.get_points()[::-1]) # Reverse
self.set_points(inner_arc.get_points()[::-1]) # Reverse
self.add_line_to(outer_arc.get_points()[0])
self.append_points(outer_arc.get_points())
self.add_subpath(outer_arc.get_points())
self.add_line_to(inner_arc.get_points()[-1])
@ -456,11 +448,14 @@ class Annulus(VMobject):
)
self.radius = outer_radius
outer_circle = Circle(radius=outer_radius)
inner_circle = Circle(radius=inner_radius)
inner_circle.reverse_points()
self.append_points(outer_circle.get_points())
self.append_points(inner_circle.get_points())
# Make sure to add enough components that triangulation doesn't fail
kw = dict(
n_components=int(max(8, np.ceil(TAU / math.acos(inner_radius / outer_radius))))
)
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0, **kw)
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0, **kw)
self.add_subpath(outer_path)
self.add_subpath(inner_path)
self.shift(center)
@ -720,6 +715,7 @@ class Arrow(Line):
else:
alpha = tip_len / arc_len
self.pointwise_become_partial(self, 0, 1 - alpha)
self.start_new_path(self.get_points()[-1])
self.add_line_to(prev_end)
return self
@ -731,12 +727,9 @@ class Arrow(Line):
self.max_width_to_length_ratio * self.get_length(),
)
widths_array = np.full(self.get_num_points(), width)
nppc = self.n_points_per_curve
if len(widths_array) > nppc:
widths_array[-nppc:] = [
a * self.tip_width_ratio * width
for a in np.linspace(1, 0, nppc)
]
if len(widths_array) > 3:
tip_width = self.tip_width_ratio * width
widths_array[-3:] = tip_width * np.linspace(1, 0, 3)
self.set_stroke(width=widths_array)
return self
@ -847,7 +840,7 @@ class FillArrow(Line):
self.add_line_to(tip_width * DOWN / 2)
self.add_line_to(points2[0])
# Close it out
self.append_points(points2)
self.add_subpath(points2)
self.add_line_to(points1[0])
if length > 0 and self.get_length() > 0:
@ -860,18 +853,18 @@ class FillArrow(Line):
axis=rotate_vector(self.get_unit_vector(), -PI / 2),
)
self.shift(start - self.get_start())
self.refresh_triangulation()
def reset_points_around_ends(self):
self.set_points_by_ends(
self.get_start().copy(), self.get_end().copy(), path_arc=self.path_arc
self.get_start().copy(),
self.get_end().copy(),
path_arc=self.path_arc
)
return self
def get_start(self) -> Vect3:
nppc = self.n_points_per_curve
points = self.get_points()
return (points[0] + points[-nppc]) / 2
return 0.5 * (points[0] + points[-3])
def get_end(self) -> Vect3:
return self.get_points()[self.tip_index]
@ -922,8 +915,13 @@ class CubicBezier(VMobject):
class Polygon(VMobject):
def __init__(self, *vertices: Vect3, **kwargs):
super().__init__(**kwargs)
def __init__(
self,
*vertices: Vect3,
flat_stroke: bool = True,
**kwargs
):
super().__init__(flat_stroke=flat_stroke, **kwargs)
self.set_points_as_corners([*vertices, vertices[0]])
def get_vertices(self) -> Vect3Array:
@ -941,20 +939,16 @@ class Polygon(VMobject):
vertices = self.get_vertices()
arcs = []
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
vect1 = v2 - v1
vect2 = v3 - v2
unit_vect1 = normalize(vect1)
unit_vect2 = normalize(vect2)
vect1 = normalize(v2 - v1)
vect2 = normalize(v3 - v2)
angle = angle_between_vectors(vect1, vect2)
# Negative radius gives concave curves
angle *= np.sign(radius)
# Distance between vertex and start of the arc
cut_off_length = radius * np.tan(angle / 2)
# Determines counterclockwise vs. clockwise
sign = np.sign(np.cross(vect1, vect2)[2])
# Negative radius gives concave curves
sign = float(np.sign(radius * cross2d(vect1, vect2)))
arc = ArcBetweenPoints(
v2 - unit_vect1 * cut_off_length,
v2 + unit_vect2 * cut_off_length,
v2 - vect1 * cut_off_length,
v2 + vect2 * cut_off_length,
angle=sign * angle,
n_components=2,
)
@ -964,20 +958,19 @@ class Polygon(VMobject):
# To ensure that we loop through starting with last
arcs = [arcs[-1], *arcs[:-1]]
for arc1, arc2 in adjacent_pairs(arcs):
self.append_points(arc1.get_points())
line = Line(arc1.get_end(), arc2.get_start())
# Make sure anchors are evenly distributed
len_ratio = line.get_length() / arc1.get_arc_length()
line.insert_n_curves(
int(arc1.get_num_curves() * len_ratio)
)
self.append_points(line.get_points())
self.add_subpath(arc1.get_points())
self.add_line_to(arc2.get_start())
return self
class Polyline(VMobject):
def __init__(self, *vertices: Vect3, **kwargs):
super().__init__(**kwargs)
def __init__(
self,
*vertices: Vect3,
flat_stroke: bool = True,
**kwargs
):
super().__init__(flat_stroke=flat_stroke, **kwargs)
self.set_points_as_corners(vertices)

View file

@ -29,6 +29,7 @@ from manimlib.utils.color import color_gradient
from manimlib.utils.color import color_to_rgb
from manimlib.utils.color import get_colormap_list
from manimlib.utils.color import rgb_to_hex
from manimlib.utils.iterables import arrays_match
from manimlib.utils.iterables import batch_by_property
from manimlib.utils.iterables import list_update
from manimlib.utils.iterables import listify
@ -66,6 +67,7 @@ class Mobject(object):
shader_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
]
aligned_data_keys = ['points']
def __init__(
self,
@ -101,6 +103,7 @@ class Mobject(object):
self._is_animating: bool = False
self.saved_state = None
self.target = None
self.bounding_box: Vect3Array = np.zeros((3, 3))
self.init_data()
self.init_uniforms()
@ -127,12 +130,11 @@ class Mobject(object):
def init_data(self):
self.data: dict[str, np.ndarray] = {
"points": np.zeros((0, 3)),
"bounding_box": np.zeros((3, 3)),
"rgbas": np.zeros((1, 4)),
}
def init_uniforms(self):
self.uniforms: dict[str, float] = {
self.uniforms: dict[str, float | np.ndarray] = {
"is_fixed_in_frame": float(self.is_fixed_in_frame),
"gloss": self.gloss,
"shadow": self.shadow,
@ -170,8 +172,8 @@ class Mobject(object):
new_length: int,
resize_func: Callable[[np.ndarray, int], np.ndarray] = resize_array
):
if new_length != len(self.data["points"]):
self.data["points"] = resize_func(self.data["points"], new_length)
for key in self.aligned_data_keys:
self.data[key] = resize_func(self.data[key], new_length)
self.refresh_bounding_box()
return self
@ -249,9 +251,9 @@ class Mobject(object):
def get_bounding_box(self) -> Vect3Array:
if self.needs_new_bounding_box:
self.data["bounding_box"] = self.compute_bounding_box()
self.bounding_box[:] = self.compute_bounding_box()
self.needs_new_bounding_box = False
return self.data["bounding_box"]
return self.bounding_box
def compute_bounding_box(self) -> Vect3Array:
all_points = np.vstack([
@ -826,7 +828,7 @@ class Mobject(object):
return self._is_animating or self.has_updaters
def set_animating_status(self, is_animating: bool, recurse: bool = True):
for mob in (*self.get_family(recurse), *self.get_ancestors(extended=True)):
for mob in (*self.get_family(recurse), *self.get_ancestors()):
mob._is_animating = is_animating
return self
@ -1593,14 +1595,16 @@ class Mobject(object):
self.align_data(mobject)
def align_data(self, mobject: Mobject) -> None:
# In case any data arrays get resized when aligned to shader data
self.refresh_shader_data()
for mob1, mob2 in zip(self.get_family(), mobject.get_family()):
# Separate out how points are treated so that subclasses
# can handle that case differently if they choose
# In case any data arrays get resized when aligned to shader data
mob1.refresh_shader_data()
mob2.refresh_shader_data()
mob1.align_points(mob2)
for key in mob1.data.keys() & mob2.data.keys():
if key == "points":
# Separate out how points are treated so that subclasses
# can handle that case differently if they choose
continue
arr1 = mob1.data[key]
arr2 = mob2.data[key]
@ -1685,10 +1689,7 @@ class Mobject(object):
if key not in mobject1.data or key not in mobject2.data:
continue
if key in ("points", "bounding_box"):
func = path_func
else:
func = interpolate
func = path_func if key == "points" else interpolate
self.data[key][:] = func(
mobject1.data[key],
@ -1701,6 +1702,9 @@ class Mobject(object):
mobject2.uniforms[key],
alpha
)
self.bounding_box[:] = path_func(
mobject1.bounding_box, mobject2.bounding_box, alpha
)
return self
def pointwise_become_partial(self, mobject, a, b):
@ -1732,7 +1736,7 @@ class Mobject(object):
for sm, sm1, sm2 in zip(self.get_family(), mobject1.get_family(), mobject2.get_family()):
keys = sm.data.keys() & sm1.data.keys() & sm2.data.keys()
sm.lock_data(list(filter(
lambda key: (sm1.data[key] == sm2.data[key]).all(),
lambda key: arrays_match(sm1.data[key], sm2.data[key]),
keys,
)))
return self

View file

@ -486,17 +486,29 @@ class VectorizedEarth(SVGMobject):
class Piano(VGroup):
n_white_keys = 52
black_pattern = [0, 2, 3, 5, 6]
white_keys_per_octave = 7
white_key_dims = (0.15, 1.0)
black_key_dims = (0.1, 0.66)
key_buff = 0.02
white_key_color = WHITE
black_key_color = GREY_E
total_width = 13
def __init__(
self,
n_white_keys = 52,
black_pattern = [0, 2, 3, 5, 6],
white_keys_per_octave = 7,
white_key_dims = (0.15, 1.0),
black_key_dims = (0.1, 0.66),
key_buff = 0.02,
white_key_color = WHITE,
black_key_color = GREY_E,
total_width = 13,
**kwargs
):
self.n_white_keys = n_white_keys
self.black_pattern = black_pattern
self.white_keys_per_octave = white_keys_per_octave
self.white_key_dims = white_key_dims
self.black_key_dims = black_key_dims
self.key_buff = key_buff
self.white_key_color = white_key_color
self.black_key_color = black_key_color
self.total_width = total_width
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add_white_keys()
self.add_black_keys()
@ -563,3 +575,4 @@ class Piano3D(VGroup):
for i, key in enumerate(self):
if piano_2d[i] in piano_2d.black_keys:
key.shift(black_key_shift * OUT)
key.set_color(BLACK)

View file

@ -49,10 +49,6 @@ class StringMobject(SVGMobject, ABC):
fill_color: ManimColor = WHITE,
stroke_color: ManimColor = WHITE,
stroke_width: float = 0,
path_string_config: dict = dict(
should_subdivide_sharp_curves=True,
should_remove_null_curves=True,
),
base_color: ManimColor = WHITE,
isolate: Selector = (),
protect: Selector = (),
@ -73,7 +69,6 @@ class StringMobject(SVGMobject, ABC):
stroke_color=stroke_color,
fill_color=fill_color,
stroke_width=stroke_width,
path_string_config=path_string_config,
**kwargs
)
self.labels = [submob.label for submob in self.submobjects]

View file

@ -23,11 +23,13 @@ from manimlib.utils.simple_functions import hash_string
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import ManimColor
from typing import Tuple
from manimlib.typing import ManimColor, Vect3Array
SVG_HASH_TO_MOB_MAP: dict[int, list[VMobject]] = {}
PATH_TO_POINTS: dict[str, Tuple[Vect3Array, np.ndarray]] = {}
def _convert_point_to_3d(x: float, y: float) -> np.ndarray:
@ -305,15 +307,10 @@ class VMobjectFromSVGPath(VMobject):
def init_points(self) -> None:
# After a given svg_path has been converted into points, the result
# will be saved to a file so that future calls for the same path
# don't need to retrace the same computation.
# will be saved so that future calls for the same pathdon't need to
# retrace the same computation.
path_string = self.path_obj.d()
path_hash = hash_string(path_string)
points_filepath = os.path.join(get_mobject_data_dir(), f"{path_hash}_points.npy")
if os.path.exists(points_filepath):
self.set_points(np.load(points_filepath))
else:
if path_string not in PATH_TO_POINTS:
self.handle_commands()
if self.should_subdivide_sharp_curves:
# For a healthy triangulation later
@ -321,8 +318,18 @@ class VMobjectFromSVGPath(VMobject):
if self.should_remove_null_curves:
# Get rid of any null curves
self.set_points(self.get_points_without_null_curves())
# Save to a file for future use
np.save(points_filepath, self.get_points())
# So triangulation doesn't get messed up
self.subdivide_intersections()
# Save for future use
PATH_TO_POINTS[path_string] = (
self.get_points().copy(),
self.get_triangulation().copy()
)
else:
points, triangulation = PATH_TO_POINTS[path_string]
self.set_points(points)
self.triangulation = triangulation
self.needs_new_triangulation = False
def handle_commands(self) -> None:
segment_class_to_func_map = {
@ -343,4 +350,4 @@ class VMobjectFromSVGPath(VMobject):
# Get rid of the side effect of trailing "Z M" commands.
if self.has_new_path_started():
self.resize_points(self.get_num_points() - 1)
self.resize_points(self.get_num_points() - 2)

View file

@ -417,9 +417,18 @@ class Text(MarkupText):
# For backward compatibility
isolate: Selector = (re.compile(r"\w+", re.U), re.compile(r"\S+", re.U)),
use_labelled_svg: bool = True,
path_string_config: dict = dict(
use_simple_quadratic_approx=True,
),
**kwargs
):
super().__init__(text, isolate=isolate, use_labelled_svg=use_labelled_svg, **kwargs)
super().__init__(
text,
isolate=isolate,
use_labelled_svg=use_labelled_svg,
path_string_config=path_string_config,
**kwargs
)
@staticmethod
def get_command_matches(string: str) -> list[re.Match]:

View file

@ -36,19 +36,19 @@ class SurfaceMesh(VGroup):
stroke_width: float = 1,
stroke_color: ManimColor = GREY_A,
normal_nudge: float = 1e-2,
flat_stroke: bool = False,
depth_test: bool = True,
joint_type: str = 'no_joint',
**kwargs
):
self.uv_surface = uv_surface
self.resolution = resolution
self.normal_nudge = normal_nudge
self.flat_stroke = flat_stroke
super().__init__(
stroke_color=stroke_color,
stroke_width=stroke_width,
depth_test=depth_test,
joint_type=joint_type,
**kwargs
)
@ -291,7 +291,7 @@ class VGroup3D(VGroup):
gloss: float = 0.2,
shadow: float = 0.2,
reflectiveness: float = 0.2,
joint_type: str = "round",
joint_type: str = "no_joint",
**kwargs
):
super().__init__(*vmobjects, **kwargs)
@ -397,11 +397,14 @@ class Prismify(VGroup3D):
# At the moment, this assume stright edges
vect = depth * direction
pieces = [vmobject.copy()]
points = vmobject.get_points()[::vmobject.n_points_per_curve]
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])
pieces.append(wall)
pieces.append(vmobject.copy().shift(vect).reverse_points())
top = vmobject.copy()
top.shift(vect)
top.reverse_points()
pieces.append(top)
super().__init__(*pieces, **kwargs)

View file

@ -23,8 +23,6 @@ class PMobject(Mobject):
):
# TODO
for key in self.data:
if key == "bounding_box":
continue
if len(self.data[key]) != size:
self.data[key] = resize_func(self.data[key], size)
return self
@ -82,8 +80,6 @@ class PMobject(Mobject):
for mob in self.family_members_with_points():
to_keep = ~np.apply_along_axis(condition, 1, mob.get_points())
for key in mob.data:
if key == "bounding_box":
continue
mob.data[key] = mob.data[key][to_keep]
return self
@ -115,8 +111,6 @@ class PMobject(Mobject):
lower_index = int(a * pmobject.get_num_points())
upper_index = int(b * pmobject.get_num_points())
for key in self.data:
if key == "bounding_box":
continue
self.data[key] = pmobject.data[key][lower_index:upper_index].copy()
return self

View file

@ -66,6 +66,10 @@ class Surface(Mobject):
)
self.compute_triangle_indices()
def init_uniforms(self):
super().init_uniforms()
self.uniforms["clip_plane"] = np.zeros(4)
def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
# To be implemented in subclasses
return (u, v, 0.0)
@ -190,13 +194,10 @@ class Surface(Mobject):
def sort_faces_back_to_front(self, vect: Vect3 = OUT):
tri_is = self.triangle_indices
indices = list(range(len(tri_is) // 3))
points = self.get_points()
def index_dot(index):
return np.dot(points[tri_is[3 * index]], vect)
indices.sort(key=index_dot)
dots = (points[tri_is[::3]] * vect).sum(1)
indices = np.argsort(dots)
for k in range(3):
tri_is[k::3] = tri_is[k::3][indices]
return self
@ -207,6 +208,23 @@ class Surface(Mobject):
surface.sort_faces_back_to_front(vect)
self.add_updater(updater)
def set_clip_plane(
self,
vect: Vect3 | None = None,
threshold: float | None = None
):
if vect is not None:
self.uniforms["clip_plane"][:3] = vect
if threshold is not None:
self.uniforms["clip_plane"][3] = threshold
self.shader_wrapper.use_clip_plane = True
return self
def deactivate_clip_plane(self):
self.uniforms["clip_plane"][:] = 0
self.shader_wrapper.use_clip_plane = False
return self
# For shaders
def get_shader_data(self) -> np.ndarray:
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()

View file

@ -1,9 +1,6 @@
from __future__ import annotations
from functools import reduce
from functools import wraps
import itertools as it
import operator as op
import moderngl
import numpy as np
@ -23,7 +20,9 @@ from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import interpolate
from manimlib.utils.bezier import inverse_interpolate
from manimlib.utils.bezier import find_intersection
from manimlib.utils.bezier import partial_quadratic_bezier_points
from manimlib.utils.bezier import outer_interpolate
from manimlib.utils.color import color_gradient
from manimlib.utils.color import rgb_to_hex
from manimlib.utils.iterables import listify
@ -35,7 +34,11 @@ from manimlib.utils.space_ops import cross2d
from manimlib.utils.space_ops import earclip_triangulation
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import get_unit_normal
from manimlib.utils.space_ops import line_intersects_path
from manimlib.utils.space_ops import midpoint
from manimlib.utils.space_ops import normalize_along_axis
from manimlib.utils.space_ops import z_to_vector
from manimlib.utils.simple_functions import arr_clip
from manimlib.shader_wrapper import ShaderWrapper
from typing import TYPE_CHECKING
@ -48,9 +51,8 @@ DEFAULT_STROKE_COLOR = GREY_A
DEFAULT_FILL_COLOR = GREY_C
class VMobject(Mobject):
n_points_per_curve: int = 3
stroke_shader_folder: str = "quadratic_bezier_stroke"
fill_shader_folder: str = "quadratic_bezier_fill"
stroke_shader_folder: str = "quadratic_bezier_stroke"
fill_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
('orientation', np.float32, (1,)),
@ -59,12 +61,13 @@ class VMobject(Mobject):
]
stroke_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
("point", np.float32, (3,)),
("prev_point", np.float32, (3,)),
("next_point", np.float32, (3,)),
("joint_angle", np.float32, (1,)),
("stroke_width", np.float32, (1,)),
("color", np.float32, (4,)),
]
render_primitive: int = moderngl.TRIANGLES
fill_render_primitive: int = moderngl.TRIANGLES
stroke_render_primitive: int = moderngl.TRIANGLE_STRIP
aligned_data_keys = ["points", "orientation", "joint_angle"]
pre_function_handle_to_anchor_scale_factor: float = 0.01
make_smooth_after_applying_functions: bool = False
@ -82,9 +85,10 @@ class VMobject(Mobject):
draw_stroke_behind_fill: bool = False,
background_image_file: str | None = None,
long_lines: bool = False,
# Could also be "bevel", "miter", "round"
# Could also be "no_joint", "bevel", "miter"
joint_type: str = "auto",
flat_stroke: bool = False,
use_simple_quadratic_approx: bool = False,
# Measured in pixel widths
anti_alias_width: float = 1.0,
**kwargs
@ -99,10 +103,13 @@ class VMobject(Mobject):
self.long_lines = long_lines
self.joint_type = joint_type
self.flat_stroke = flat_stroke
self.use_simple_quadratic_approx = use_simple_quadratic_approx
self.anti_alias_width = anti_alias_width
self.needs_new_triangulation = True
self.triangulation = np.zeros(0, dtype='i4')
self.needs_new_joint_angles = True
self.outer_vert_indices = np.zeros(0, dtype='i4')
super().__init__(**kwargs)
@ -117,6 +124,7 @@ class VMobject(Mobject):
"stroke_rgba": np.zeros((1, 4)),
"stroke_width": np.zeros((1, 1)),
"orientation": np.ones((1, 1)),
"joint_angle": np.zeros((0, 1)),
})
def init_uniforms(self):
@ -415,22 +423,23 @@ class VMobject(Mobject):
# Points
def set_anchors_and_handles(
self,
anchors1: Vect3Array,
anchors: Vect3Array,
handles: Vect3Array,
anchors2: Vect3Array
):
assert(len(anchors1) == len(handles) == len(anchors2))
nppc = self.n_points_per_curve
new_points = np.zeros((nppc * len(anchors1), self.dim))
arrays = [anchors1, handles, anchors2]
for index, array in enumerate(arrays):
new_points[index::nppc] = array
self.set_points(new_points)
assert(len(anchors) == len(handles) + 1)
points = resize_array(self.get_points(), 2 * len(anchors) - 1)
points[0::2] = anchors
points[1::2] = handles
self.set_points(points)
return self
def start_new_path(self, point: Vect3):
assert(self.get_num_points() % self.n_points_per_curve == 0)
self.append_points([point])
# Path ends are signaled by a handle point sitting directly
# on top of the previous anchor
if self.has_points():
self.append_points([self.get_last_point(), point])
else:
self.set_points([point])
return self
def add_cubic_bezier_curve(
@ -440,54 +449,54 @@ class VMobject(Mobject):
handle2: Vect3,
anchor2: Vect3
):
new_points = get_quadratic_approximation_of_cubic(anchor1, handle1, handle2, anchor2)
self.append_points(new_points)
self.add_subpath(get_quadratic_approximation_of_cubic(
anchor1, handle1, handle2, anchor2
))
return self
def add_cubic_bezier_curve_to(
self,
handle1: Vect3,
handle2: Vect3,
anchor: Vect3
anchor: Vect3,
):
"""
Add cubic bezier curve to the path.
"""
self.throw_error_if_no_points()
quadratic_approx = get_quadratic_approximation_of_cubic(
self.get_last_point(), handle1, handle2, anchor
)
if self.has_new_path_started():
self.append_points(quadratic_approx[1:])
last = self.get_last_point()
# If the two relevant tangents are close in angle to each other,
# then just approximate with a single quadratic bezier curve.
# Otherwise, approximate with two
v1 = handle1 - last
v2 = anchor - handle2
angle = angle_between_vectors(v1, v2)
if self.use_simple_quadratic_approx and angle < 45 * DEGREES:
quadratic_approx = [last, find_intersection(last, v1, anchor, -v2), anchor]
else:
self.append_points(quadratic_approx)
quadratic_approx = get_quadratic_approximation_of_cubic(
last, handle1, handle2, anchor
)
if self.consider_points_equal(quadratic_approx[1], last):
# This is to prevent subpaths from accidentally being marked closed
quadratic_approx[1] = midpoint(*quadratic_approx[1:3])
self.append_points(quadratic_approx[1:])
return self
def add_quadratic_bezier_curve_to(self, handle: Vect3, anchor: Vect3):
self.throw_error_if_no_points()
if self.has_new_path_started():
self.append_points([handle, anchor])
else:
self.append_points([self.get_last_point(), handle, anchor])
last_point = self.get_last_point()
if self.consider_points_equal(handle, last_point):
# This is to prevent subpaths from accidentally being marked closed
handle = midpoint(handle, anchor)
self.append_points([handle, anchor])
return self
def add_line_to(self, point: Vect3):
end = self.get_points()[-1]
alphas = np.linspace(0, 1, self.n_points_per_curve)
if self.long_lines:
halfway = interpolate(end, point, 0.5)
points = [
interpolate(end, halfway, a)
for a in alphas
] + [
interpolate(halfway, point, a)
for a in alphas
]
else:
points = [
interpolate(end, point, a)
for a in alphas
]
if self.has_new_path_started():
points = points[1:]
self.append_points(points)
self.throw_error_if_no_points()
last_point = self.get_last_point()
alphas = np.linspace(0, 1, 5 if self.long_lines else 3)
self.append_points(outer_interpolate(last_point, point, alphas[1:]))
return self
def add_smooth_curve_to(self, point: Vect3):
@ -502,13 +511,16 @@ class VMobject(Mobject):
def add_smooth_cubic_curve_to(self, handle: Vect3, point: Vect3):
self.throw_error_if_no_points()
if self.get_num_points() == 1:
new_handle = self.get_points()[-1]
new_handle = handle
else:
new_handle = self.get_reflection_of_last_handle()
self.add_cubic_bezier_curve_to(new_handle, handle, point)
def has_new_path_started(self) -> bool:
return self.get_num_points() % self.n_points_per_curve == 1
points = self.get_points()
if len(points) == 1:
return True
return self.consider_points_equal(points[-3], points[-2])
def get_last_point(self) -> Vect3:
return self.get_points()[-1]
@ -517,35 +529,62 @@ class VMobject(Mobject):
points = self.get_points()
return 2 * points[-1] - points[-2]
def close_path(self):
if not self.is_closed():
self.add_line_to(self.get_subpaths()[-1][0])
def close_path(self, smooth: bool = False):
if self.is_closed():
return self
last_path_start = self.get_subpaths()[-1][0]
if smooth:
self.add_smooth_curve_to(last_path_start)
else:
self.add_line_to(last_path_start)
return self
def is_closed(self) -> bool:
return self.consider_points_equals(
self.get_points()[0], self.get_points()[-1]
)
points = self.get_points()
return self.consider_points_equal(points[0], points[-1])
def subdivide_curves_by_condition(
self,
tuple_to_subdivisions: Callable,
recurse: bool = True
):
for vmob in self.get_family(recurse):
if not vmob.has_points():
continue
new_points = [vmob.get_points()[0]]
for tup in vmob.get_bezier_tuples():
n_divisions = tuple_to_subdivisions(*tup)
if n_divisions > 0:
alphas = np.linspace(0, 1, n_divisions + 2)
new_points.extend([
partial_quadratic_bezier_points(tup, a1, a2)[1:]
for a1, a2 in zip(alphas, alphas[1:])
])
else:
new_points.append(tup[1:])
vmob.set_points(np.vstack(new_points))
return self
def subdivide_sharp_curves(
self,
angle_threshold: float = 30 * DEGREES,
recurse: bool = True
):
vmobs = [vm for vm in self.get_family(recurse) if vm.has_points()]
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_quadratic_bezier_points(tup, a1, a2)
for a1, a2 in zip(alphas, alphas[1:])
])
else:
new_points.append(tup)
vmob.set_points(np.vstack(new_points))
def tuple_to_subdivisions(b0, b1, b2):
angle = angle_between_vectors(b1 - b0, b2 - b1)
return int(angle / angle_threshold)
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
return self
def subdivide_intersections(self, recurse: bool = True, n_subdivisions: int = 1):
path = self.get_anchors()
def tuple_to_subdivisions(b0, b1, b2):
if line_intersects_path(b0, b1, path):
return n_subdivisions
return 0
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
return self
def add_points_as_corners(self, points: Iterable[Vect3]):
@ -554,12 +593,9 @@ class VMobject(Mobject):
return points
def set_points_as_corners(self, points: Iterable[Vect3]):
nppc = self.n_points_per_curve
points = np.array(points)
self.set_anchors_and_handles(*[
interpolate(points[:-1], points[1:], a)
for a in np.linspace(0, 1, nppc)
])
anchors = np.array(points)
handles = 0.5 * (anchors[:-1] + anchors[1:])
self.set_anchors_and_handles(anchors, handles)
return self
def set_points_smoothly(
@ -576,21 +612,29 @@ class VMobject(Mobject):
def change_anchor_mode(self, mode: str):
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
nppc = self.n_points_per_curve
for submob in self.family_members_with_points():
subpaths = submob.get_subpaths()
submob.clear_points()
new_points = []
for subpath in subpaths:
anchors = np.vstack([subpath[::nppc], subpath[-1:]])
anchors = subpath[::2]
new_subpath = np.array(subpath)
if mode == "approx_smooth":
new_subpath[1::nppc] = get_smooth_quadratic_bezier_handle_points(anchors)
new_subpath[1::2] = get_smooth_quadratic_bezier_handle_points(anchors)
elif mode == "true_smooth":
h1, h2 = get_smooth_cubic_bezier_handle_points(anchors)
new_subpath = get_quadratic_approximation_of_cubic(anchors[:-1], h1, h2, anchors[1:])
# The format here is that each successive group of 5 points
# represents two quadratic bezier curves. We assume the end
# of one is the start of the next, so eliminate elements 5, 10, 15, etc.
quads = get_quadratic_approximation_of_cubic(anchors[:-1], h1, h2, anchors[1:])
is_start = (np.arange(len(quads)) % 5 == 0)
new_subpath = np.array([quads[0], *quads[~is_start]])
elif mode == "jagged":
new_subpath[1::nppc] = 0.5 * (anchors[:-1] + anchors[1:])
submob.append_points(new_subpath)
new_subpath[1::2] = 0.5 * (anchors[:-1] + anchors[1:])
if new_points:
# Close previous path
new_points.append(new_points[-1][-1])
new_points.append(new_subpath)
submob.set_points(np.vstack(new_points))
submob.refresh_triangulation()
return self
@ -620,66 +664,63 @@ class VMobject(Mobject):
return self
def add_subpath(self, points: Vect3Array):
assert(len(points) % self.n_points_per_curve == 0)
self.append_points(points)
assert(len(points) % 2 == 1)
if not self.has_points():
self.set_points(points)
return self
if not self.consider_points_equal(points[0], self.get_points()[-1]):
self.start_new_path(points[0])
self.append_points(points[1:])
return self
def append_vectorized_mobject(self, vectorized_mobject: VMobject):
new_points = list(vectorized_mobject.get_points())
if self.has_new_path_started():
# Remove last point, which is starting
# a new path
self.resize_points(len(self.get_points() - 1))
self.append_points(new_points)
def append_vectorized_mobject(self, vmobject: VMobject):
self.add_subpath(vmobject.get_points())
return self
#
def consider_points_equals(self, p0: Vect3, p1: Vect3) -> bool:
def consider_points_equal(self, p0: Vect3, p1: Vect3) -> bool:
return get_norm(p1 - p0) < self.tolerance_for_point_equality
# Information about the curve
def get_bezier_tuples_from_points(self, points: Sequence[Vect3]):
nppc = self.n_points_per_curve
remainder = len(points) % nppc
points = points[:len(points) - remainder]
return (
points[i:i + nppc]
for i in range(0, len(points), nppc)
)
def get_bezier_tuples_from_points(self, points: Vect3Array) -> Iterable[Vect3Array]:
n_curves = (len(points) - 1) // 2
return (points[2 * i : 2 * i + 3] for i in range(n_curves))
def get_bezier_tuples(self):
def get_bezier_tuples(self) -> Iterable[Vect3Array]:
return self.get_bezier_tuples_from_points(self.get_points())
def get_subpaths_from_points(
self,
points: Vect3Array
) -> list[Vect3Array]:
nppc = self.n_points_per_curve
diffs = points[nppc - 1:-1:nppc] - points[nppc::nppc]
splits = (diffs * diffs).sum(1) > self.tolerance_for_point_equality
split_indices = np.arange(nppc, len(points), nppc, dtype=int)[splits]
def get_subpath_end_indices_from_points(self, points: Vect3Array):
atol = self.tolerance_for_point_equality
a0, h, a1 = points[0:-1:2], points[1::2], points[2::2]
# An anchor point is considered the end of a path
# if its following handle is sitting on top of it.
# To disambiguate this from cases with many null
# curves in a row, we also check that the following
# anchor is genuinely distinct
is_end = (a0 == h).all(1) & (abs(h - a1) > atol).any(1)
inner_ends = (2 * n for n, end in enumerate(is_end) if end)
return np.array([*inner_ends, len(points) - 1])
split_indices = [0, *split_indices, len(points)]
return [
points[i1:i2]
for i1, i2 in zip(split_indices, split_indices[1:])
if (i2 - i1) >= nppc
]
def get_subpath_end_indices(self):
return self.get_subpath_end_indices_from_points(self.get_points())
def get_subpaths_from_points(self, points: Vect3Array) -> list[Vect3Array]:
end_indices = self.get_subpath_end_indices_from_points(points)
start_indices = [0, *(end_indices[:-1] + 2)]
return [points[i1:i2 + 1] for i1, i2 in zip(start_indices, end_indices)]
def get_subpaths(self) -> list[Vect3Array]:
return self.get_subpaths_from_points(self.get_points())
def get_nth_curve_points(self, n: int) -> Vect3:
def get_nth_curve_points(self, n: int) -> Vect3Array:
assert(n < self.get_num_curves())
nppc = self.n_points_per_curve
return self.get_points()[nppc * n:nppc * (n + 1)]
return self.get_points()[2 * n : 2 * n + 3]
def get_nth_curve_function(self, n: int) -> Callable[[float], Vect3]:
return bezier(self.get_nth_curve_points(n))
def get_num_curves(self) -> int:
return self.get_num_points() // self.n_points_per_curve
return len(self.data["points"]) // 2
def quick_point_from_proportion(self, alpha: float) -> Vect3:
# Assumes all curves have the same length, so is inaccurate
@ -694,20 +735,24 @@ class VMobject(Mobject):
elif alpha >= 1:
return self.get_end()
partials = [0]
partials: list[float] = [0]
for tup in self.get_bezier_tuples():
# Approximate length with straight line from start to end
arclen = get_norm(tup[0] - tup[-1])
if self.consider_points_equal(tup[0], tup[1]):
# Don't consider null curves
arclen = 0
else:
# Approximate length with straight line from start to end
arclen = get_norm(tup[2] - tup[0])
partials.append(partials[-1] + arclen)
full = partials[-1]
if full == 0:
return self.get_start()
# First index where the partial length is more alpha times the full length
# First index where the partial length is more than alpha times the full length
i = next(
(i for i, x in enumerate(partials) if x >= full * alpha),
len(partials) # Default
)
residue = inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha)
residue = float(inverse_interpolate(partials[i - 1] / full, partials[i] / full, alpha))
return self.get_nth_curve_function(i - 1)(residue)
def get_anchors_and_handles(self) -> list[Vect3]:
@ -717,37 +762,24 @@ class VMobject(Mobject):
will be three points defining a quadratic bezier curve
for any i in range(0, len(anchors1))
"""
nppc = self.n_points_per_curve
points = self.get_points()
return [
points[i::nppc]
for i in range(nppc)
]
return [points[0:-1:2], points[1::2], points[2::2]]
def get_start_anchors(self) -> Vect3Array:
return self.get_points()[0::self.n_points_per_curve]
return self.get_points()[0:-1:2]
def get_end_anchors(self) -> Vect3:
nppc = self.n_points_per_curve
return self.get_points()[nppc - 1::nppc]
return self.get_points()[2::2]
def get_anchors(self) -> Vect3Array:
points = self.get_points()
if len(points) == 1:
return points
return np.array(list(it.chain(*zip(
self.get_start_anchors(),
self.get_end_anchors(),
))))
return self.get_points()[::2]
def get_points_without_null_curves(self, atol: float = 1e-9) -> Vect3Array:
nppc = self.n_points_per_curve
points = self.get_points()
distinct_curves = reduce(op.or_, [
(abs(points[i::nppc] - points[0::nppc]) > atol).any(1)
for i in range(1, nppc)
])
return points[distinct_curves.repeat(nppc)]
new_points = [self.get_points()[0]]
for tup in self.get_bezier_tuples():
if get_norm(tup[1] - tup[0]) > atol or get_norm(tup[2] - tup[0]) > atol:
new_points.append(tup[1:])
return np.vstack(new_points)
def get_arc_length(self, n_sample_points: int | None = None) -> float:
if n_sample_points is None:
@ -757,8 +789,7 @@ class VMobject(Mobject):
for a in np.linspace(0, 1, n_sample_points)
])
diffs = points[1:] - points[:-1]
norms = np.array([get_norm(d) for d in diffs])
return norms.sum()
return sum(map(get_norm, diffs))
def get_area_vector(self) -> Vect3:
# Returns a vector whose length is the area bound by
@ -768,21 +799,16 @@ class VMobject(Mobject):
if not self.has_points():
return np.zeros(3)
nppc = self.n_points_per_curve
points = self.get_points()
p0 = points[0::nppc]
p1 = points[nppc - 1::nppc]
p0 = self.get_anchors()
p1 = np.vstack([p0[1:], p0[0]])
if len(p0) != len(p1):
m = min(len(p0), len(p1))
p0 = p0[:m]
p1 = p1[:m]
# Each term goes through all edges [(x1, y1, z1), (x2, y2, z2)]
# Each term goes through all edges [(x0, y0, z0), (x1, y1, z1)]
sums = p0 + p1
diffs = p1 - p0
return 0.5 * np.array([
sum((p0[:, 1] + p1[:, 1]) * (p1[:, 2] - p0[:, 2])), # Add up (y1 + y2)*(z2 - z1)
sum((p0[:, 2] + p1[:, 2]) * (p1[:, 0] - p0[:, 0])), # Add up (z1 + z2)*(x2 - x1)
sum((p0[:, 0] + p1[:, 0]) * (p1[:, 1] - p0[:, 1])), # Add up (x1 + x2)*(y2 - y1)
(sums[:, 1] * diffs[:, 2]).sum(), # Add up (y0 + y1)*(z1 - z0)
(sums[:, 2] * diffs[:, 0]).sum(), # Add up (z0 + z1)*(x1 - x0)
(sums[:, 0] * diffs[:, 1]).sum(), # Add up (x0 + x1)*(y1 - y0)
])
def get_unit_normal(self) -> Vect3:
@ -809,41 +835,40 @@ class VMobject(Mobject):
# needlessly throughout an animation
if self.has_fill() and vmobject.has_fill() and self.has_same_shape_as(vmobject):
vmobject.triangulation = self.triangulation
return
return self
for mob in self, vmobject:
# If there are no points, add one to
# where the "center" is
if not mob.has_points():
mob.start_new_path(mob.get_center())
# If there's only one point, turn it into
# a null curve
if mob.has_new_path_started():
mob.add_line_to(mob.get_points()[0])
# Figure out what the subpaths are, and align
subpaths1 = self.get_subpaths()
subpaths2 = vmobject.get_subpaths()
n_subpaths = max(len(subpaths1), len(subpaths2))
# Start building new ones
new_subpaths1 = []
new_subpaths2 = []
nppc = self.n_points_per_curve
def get_nth_subpath(path_list, n):
if n >= len(path_list):
# Create a null path at the very end
return [path_list[-1][-1]] * nppc
return [path_list[-1][-1]] * 3
return path_list[n]
for n in range(n_subpaths):
sp1 = get_nth_subpath(subpaths1, n)
sp2 = get_nth_subpath(subpaths2, n)
diff1 = max(0, (len(sp2) - len(sp1)) // nppc)
diff2 = max(0, (len(sp1) - len(sp2)) // nppc)
diff1 = max(0, (len(sp2) - len(sp1)) // 2)
diff2 = max(0, (len(sp1) - len(sp2)) // 2)
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
sp2 = self.insert_n_curves_to_point_list(diff2, sp2)
if n > 0:
# Add intermediate anchor to mark path end
new_subpaths1.append(new_subpaths1[0][-1])
new_subpaths2.append(new_subpaths2[0][-1])
new_subpaths1.append(sp1)
new_subpaths2.append(sp2)
self.set_points(np.vstack(new_subpaths1))
@ -854,26 +879,23 @@ class VMobject(Mobject):
for mob in self.get_family(recurse):
if mob.get_num_curves() > 0:
new_points = mob.insert_n_curves_to_point_list(n, mob.get_points())
# TODO, this should happen in insert_n_curves_to_point_list
if mob.has_new_path_started():
new_points = np.vstack([new_points, mob.get_last_point()])
mob.set_points(new_points)
return self
def insert_n_curves_to_point_list(self, n: int, points: Vect3Array):
nppc = self.n_points_per_curve
if len(points) == 1:
return np.repeat(points, nppc * n, 0)
return np.repeat(points, 2 * n + 1, 0)
bezier_groups = list(self.get_bezier_tuples_from_points(points))
bezier_tuples = list(self.get_bezier_tuples_from_points(points))
atol = self.tolerance_for_point_equality
norms = np.array([
get_norm(bg[nppc - 1] - bg[0])
for bg in bezier_groups
0 if get_norm(tup[1] - tup[0]) < atol else get_norm(tup[2] - tup[0])
for tup in bezier_tuples
])
total_norm = sum(norms)
# Calculate insertions per curve (ipc)
if total_norm < 1e-6:
ipc = [n] + [0] * (len(bezier_groups) - 1)
ipc = [n] + [0] * (len(bezier_tuples) - 1)
else:
ipc = np.round(n * norms / sum(norms)).astype(int)
@ -883,14 +905,14 @@ class VMobject(Mobject):
for x in range(-diff):
ipc[np.argmax(ipc)] -= 1
new_points = []
for group, n_inserts in zip(bezier_groups, ipc):
new_points = [points[0]]
for tup, n_inserts in zip(bezier_tuples, ipc):
# What was once a single quadratic curve defined
# by "group" will now be broken into n_inserts + 1
# by "tup" will now be broken into n_inserts + 1
# smaller quadratic curves
alphas = np.linspace(0, 1, n_inserts + 2)
for a1, a2 in zip(alphas, alphas[1:]):
new_points += partial_quadratic_bezier_points(group, a1, a2)
new_points.extend(partial_quadratic_bezier_points(tup, a1, a2)[1:])
return np.vstack(new_points)
def interpolate(
@ -910,25 +932,24 @@ class VMobject(Mobject):
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float):
assert(isinstance(vmobject, VMobject))
vm_points = vmobject.get_points()
if a <= 0 and b >= 1:
self.become(vmobject)
self.set_points(vm_points, refresh=False)
return self
num_curves = vmobject.get_num_curves()
nppc = self.n_points_per_curve
# Partial curve includes three portions:
# - A middle section, which matches the curve exactly
# - A start, which is some ending portion of an inner quadratic
# - A middle section, which matches the curve exactly
# - An end, which is the starting portion of a later inner quadratic
lower_index, lower_residue = integer_interpolate(0, num_curves, a)
upper_index, upper_residue = integer_interpolate(0, num_curves, b)
i1 = nppc * lower_index
i2 = nppc * (lower_index + 1)
i3 = nppc * upper_index
i4 = nppc * (upper_index + 1)
i1 = 2 * lower_index
i2 = 2 * lower_index + 3
i3 = 2 * upper_index
i4 = 2 * upper_index + 3
vm_points = vmobject.get_points()
new_points = vm_points.copy()
if num_curves == 0:
new_points[:] = 0
@ -938,7 +959,6 @@ class VMobject(Mobject):
new_points[:i1] = tup[0]
new_points[i1:i4] = tup
new_points[i4:] = tup[2]
new_points[nppc:] = new_points[nppc - 1]
else:
low_tup = partial_quadratic_bezier_points(vm_points[i1:i2], lower_residue, 1)
high_tup = partial_quadratic_bezier_points(vm_points[i3:i4], 0, upper_residue)
@ -947,7 +967,9 @@ class VMobject(Mobject):
# Keep new_points i2:i3 as they are
new_points[i3:i4] = high_tup
new_points[i4:] = high_tup[2]
self.set_points(new_points)
self.set_points(new_points, refresh=False)
if self.has_fill():
self.refresh_triangulation()
return self
def get_subcurve(self, a: float, b: float) -> VMobject:
@ -955,7 +977,16 @@ class VMobject(Mobject):
vmob.pointwise_become_partial(self, a, b)
return vmob
# Related to triangulation
def get_outer_vert_indices(self):
"""
Returns the pattern (0, 1, 2, 2, 3, 4, 4, 5, 6, ...)
"""
n_curves = self.get_num_curves()
if len(self.outer_vert_indices) != 3 * n_curves:
self.outer_vert_indices = (np.arange(1, 3 * n_curves + 1) * 2) // 3
return self.outer_vert_indices
# Data for shaders that may need refreshing
def refresh_triangulation(self):
for mob in self.get_family():
@ -981,63 +1012,129 @@ class VMobject(Mobject):
return self.triangulation
normal_vector = self.get_unit_normal()
indices = np.arange(len(points), dtype=int)
# Rotate points such that unit normal vector is OUT
if not np.isclose(normal_vector, OUT).all():
points = np.dot(points, z_to_vector(normal_vector))
atol = self.tolerance_for_point_equality
end_of_loop = np.zeros(len(points) // 3, dtype=bool)
end_of_loop[:-1] = (np.abs(points[2:-3:3] - points[3::3]) > atol).any(1)
end_of_loop[-1] = True
v01s = points[1::3] - points[0::3]
v12s = points[2::3] - points[1::3]
v01s = points[1::2] - points[0:-1:2]
v12s = points[2::2] - points[1::2]
curve_orientations = np.sign(cross2d(v01s, v12s))
self.data["orientation"] = np.transpose([curve_orientations.repeat(3)])
# Reset orientation data
self.data["orientation"] = resize_array(self.data["orientation"], len(points))
self.data["orientation"][1::2, 0] = curve_orientations
if "orientation" in self.locked_data_keys:
self.locked_data_keys.remove("orientation")
concave_parts = curve_orientations < 0
# These are the vertices to which we'll apply a polygon triangulation
indices = np.arange(len(points), dtype=int)
inner_vert_indices = np.hstack([
indices[0::3],
indices[1::3][concave_parts],
indices[2::3][end_of_loop],
indices[0::2],
indices[1::2][concave_parts],
])
inner_vert_indices.sort()
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
# Even indices correspond to anchors, and `end_indices // 2`
# shows which anchors are considered end points
end_indices = self.get_subpath_end_indices()
counts = np.arange(1, len(inner_vert_indices) + 1)
rings = counts[inner_vert_indices % 2 == 0][end_indices // 2]
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[
earclip_triangulation(inner_verts, rings)
]
# Remove null triangles, coming from adjascent points
iti = inner_tri_indices
null1 = (iti[0::3] + 1 == iti[1::3]) & (iti[0::3] + 2 == iti[2::3])
null2 = (iti[0::3] - 1 == iti[1::3]) & (iti[0::3] - 2 == iti[2::3])
inner_tri_indices = iti[~(null1 | null2).repeat(3)]
tri_indices = np.hstack([indices, inner_tri_indices])
outer_tri_indices = self.get_outer_vert_indices()
tri_indices = np.hstack([outer_tri_indices, inner_tri_indices])
self.triangulation = tri_indices
self.needs_new_triangulation = False
return tri_indices
def refresh_joint_angles(self):
for mob in self.get_family():
mob.needs_new_joint_angles = True
return self
def get_joint_angles(self, refresh: bool = False):
if not self.needs_new_joint_angles and not refresh:
return self.data["joint_angle"]
self.needs_new_joint_angles = False
points = self.get_points()
self.data["joint_angle"] = resize_array(self.data["joint_angle"], len(points))
if(len(points) < 3):
return self.data["joint_angle"]
# Unit tangent vectors
a0, h, a1 = points[0:-1:2], points[1::2], points[2::2]
a0_to_h = normalize_along_axis(h - a0, 1)
h_to_a1 = normalize_along_axis(a1 - h, 1)
vect_to_vert = np.zeros(points.shape)
vect_from_vert = np.zeros(points.shape)
vect_to_vert[1::2] = a0_to_h
vect_to_vert[2::2] = h_to_a1
vect_from_vert[0:-1:2] = a0_to_h
vect_from_vert[1::2] = h_to_a1
ends = self.get_subpath_end_indices()
starts = [0, *(e + 2 for e in ends[:-1])]
for start, end in zip(starts, ends):
if self.consider_points_equal(points[start], points[end]):
vect_to_vert[start] = h_to_a1[end // 2 - 1]
vect_from_vert[end] = a0_to_h[start // 2]
# Compute angles, and read them into
# the joint_angles array
result = self.data["joint_angle"][:, 0]
dots = (vect_to_vert * vect_from_vert).sum(1)
np.arccos(dots, out=result, where=((dots <= 1) & (dots >= -1)))
# Assumes unit normal in the positive z direction
result *= np.sign(cross2d(vect_to_vert, vect_from_vert))
return result
def triggers_refreshed_triangulation(func: Callable):
@wraps(func)
def wrapper(self, *args, **kwargs):
def wrapper(self, *args, refresh=True, **kwargs):
func(self, *args, **kwargs)
self.refresh_triangulation()
if refresh:
self.refresh_triangulation()
self.refresh_joint_angles()
return wrapper
@triggers_refreshed_triangulation
def set_points(self, points: Vect3Array):
assert(len(points) == 0 or len(points) % 2 == 1)
super().set_points(points)
return self
@triggers_refreshed_triangulation
def append_points(self, points: Vect3Array):
assert(len(points) % 2 == 0)
super().append_points(points)
return self
@triggers_refreshed_triangulation
def reverse_points(self):
# This will reset which anchors are
# considered path ends
if not self.has_points():
return self
inner_ends = self.get_subpath_end_indices()[:-1]
self.data["points"][inner_ends + 1] = self.data["points"][inner_ends + 2]
super().reverse_points()
return self
@ -1059,6 +1156,10 @@ class VMobject(Mobject):
self.make_approximately_smooth()
return self
def apply_points_function(self, *args, **kwargs,):
super().apply_points_function(*args, **kwargs)
self.refresh_joint_angles()
# For shaders
def init_shader_data(self):
self.fill_data = np.zeros(0, dtype=self.fill_dtype)
@ -1068,14 +1169,15 @@ class VMobject(Mobject):
vert_indices=np.zeros(0, dtype='i4'),
uniforms=self.uniforms,
shader_folder=self.fill_shader_folder,
render_primitive=self.render_primitive,
render_primitive=self.fill_render_primitive,
)
self.stroke_shader_wrapper = ShaderWrapper(
vert_data=self.stroke_data,
uniforms=self.uniforms,
shader_folder=self.stroke_shader_folder,
render_primitive=self.render_primitive,
render_primitive=self.stroke_render_primitive,
)
self.back_stroke_shader_wrapper = self.stroke_shader_wrapper.copy()
def refresh_shader_wrapper_id(self):
for wrapper in [self.fill_shader_wrapper, self.stroke_shader_wrapper]:
@ -1083,7 +1185,7 @@ class VMobject(Mobject):
return self
def get_fill_shader_wrapper(self) -> ShaderWrapper:
self.fill_shader_wrapper.vert_indices = self.get_fill_shader_vert_indices()
self.fill_shader_wrapper.vert_indices = self.get_triangulation()
self.fill_shader_wrapper.vert_data = self.get_fill_shader_data()
self.fill_shader_wrapper.uniforms = self.get_shader_uniforms()
self.fill_shader_wrapper.depth_test = self.depth_test
@ -1097,18 +1199,26 @@ class VMobject(Mobject):
def get_shader_wrapper_list(self) -> list[ShaderWrapper]:
# Build up data lists
fill_shader_wrappers = []
stroke_shader_wrappers = []
fill_sws = []
stroke_sws = []
bstroke_sws = []
for submob in self.family_members_with_points():
if submob.has_fill():
fill_shader_wrappers.append(submob.get_fill_shader_wrapper())
fill_sws.append(submob.get_fill_shader_wrapper())
if submob.has_stroke():
stroke_shader_wrappers.append(submob.get_stroke_shader_wrapper())
if submob.draw_stroke_behind_fill:
self.draw_stroke_behind_fill = True
lst = bstroke_sws if submob.draw_stroke_behind_fill else stroke_sws
lst.append(submob.get_stroke_shader_wrapper())
self_sws = [self.fill_shader_wrapper, self.stroke_shader_wrapper]
sw_lists = [fill_shader_wrappers, stroke_shader_wrappers]
self_sws = [
self.back_stroke_shader_wrapper,
self.fill_shader_wrapper,
self.stroke_shader_wrapper
]
sw_lists = [
bstroke_sws,
fill_sws,
stroke_sws
]
for sw, sw_list in zip(self_sws, sw_lists):
if not sw_list:
sw.vert_data = resize_array(sw.vert_data, 0)
@ -1119,26 +1229,27 @@ class VMobject(Mobject):
sw.read_in(*sw_list)
sw.depth_test = any(sw.depth_test for sw in sw_list)
sw.uniforms.update(sw_list[0].uniforms)
if self.draw_stroke_behind_fill:
self_sws.reverse()
return [sw for sw in self_sws if len(sw.vert_data) > 0]
def get_stroke_shader_data(self) -> np.ndarray:
# Set data array to be one longer than number of points,
# with a dummy vertex added at the end. This is to ensure
# it can be safely stacked onto other stroke data arrays.
points = self.get_points()
if len(self.stroke_data) != len(points):
self.stroke_data = resize_array(self.stroke_data, len(points))
n = len(points)
size = n + 1 if n > 0 else 0
if len(self.stroke_data) != size:
self.stroke_data = resize_array(self.stroke_data, size)
if n == 0:
return self.stroke_data
if "points" not in self.locked_data_keys:
nppc = self.n_points_per_curve
self.stroke_data["point"] = points
self.stroke_data["prev_point"][:nppc] = points[-nppc:]
self.stroke_data["prev_point"][nppc:] = points[:-nppc]
self.stroke_data["next_point"][:-nppc] = points[nppc:]
self.stroke_data["next_point"][-nppc:] = points[:nppc]
self.read_data_to_shader(self.stroke_data, "color", "stroke_rgba")
self.read_data_to_shader(self.stroke_data, "stroke_width", "stroke_width")
self.read_data_to_shader(self.stroke_data[:n], "point", "points")
self.read_data_to_shader(self.stroke_data[:n], "color", "stroke_rgba")
self.read_data_to_shader(self.stroke_data[:n], "stroke_width", "stroke_width")
self.get_joint_angles() # Recomputes, only if refresh is needed
self.read_data_to_shader(self.stroke_data[:n], "joint_angle", "joint_angle")
self.stroke_data[-1] = self.stroke_data[-2]
return self.stroke_data
def get_fill_shader_data(self) -> np.ndarray:

View file

@ -159,14 +159,14 @@ class InteractiveScene(Scene):
pass
def get_crosshair(self):
line = Line(LEFT, RIGHT)
line.insert_n_curves(1)
lines = line.replicate(2)
lines[1].rotate(PI / 2)
crosshair = VMobject()
crosshair.set_points([*lines[0].get_points(), *lines[1].get_points()])
lines = VMobject().replicate(2)
lines[0].set_points([LEFT, ORIGIN, RIGHT])
lines[1].set_points([UP, ORIGIN, DOWN])
crosshair = VGroup(*lines)
crosshair.set_width(self.crosshair_width)
crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2, 2, 0, 2])
crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2])
crosshair.insert_n_curves(1)
crosshair.set_animating_status(True)
crosshair.fix_in_frame()
return crosshair
@ -303,14 +303,16 @@ class InteractiveScene(Scene):
))
if len(mobs) == 0:
return
self.selection.set_animating_status(True)
self.selection.add(*mobs)
for mob in mobs:
mob.set_animating_status(True)
def toggle_from_selection(self, *mobjects: Mobject):
for mob in mobjects:
if mob in self.selection:
self.selection.remove(mob)
mob.set_animating_status(False)
mob.refresh_bounding_box()
else:
self.add_to_selection(mob)
self.refresh_static_mobjects()
@ -318,6 +320,7 @@ class InteractiveScene(Scene):
def clear_selection(self):
for mob in self.selection:
mob.set_animating_status(False)
mob.refresh_bounding_box()
self.selection.set_submobjects([])
self.refresh_static_mobjects()

View file

@ -35,6 +35,7 @@ class ShaderWrapper(object):
uniforms: dict[str, float] | None = None, # A dictionary mapping names of uniform variables
texture_paths: dict[str, str] | None = None, # A dictionary mapping names to filepaths for textures.
depth_test: bool = False,
use_clip_plane: bool = False,
render_primitive: int = moderngl.TRIANGLE_STRIP,
):
self.vert_data = vert_data
@ -44,6 +45,7 @@ class ShaderWrapper(object):
self.uniforms = uniforms or dict()
self.texture_paths = texture_paths or dict()
self.depth_test = depth_test
self.use_clip_plane = use_clip_plane
self.render_primitive = str(render_primitive)
self.init_program_code()
self.refresh_id()
@ -158,15 +160,10 @@ class ShaderWrapper(object):
return self
# For caching
filename_to_code_map: dict[str, str] = {}
@lru_cache(maxsize=12)
def get_shader_code_from_file(filename: str) -> str | None:
if not filename:
return None
if filename in filename_to_code_map:
return filename_to_code_map[filename]
try:
filepath = find_file(
@ -190,7 +187,6 @@ def get_shader_code_from_file(filename: str) -> str | None:
os.path.join("inserts", line.replace("#INSERT ", ""))
)
result = result.replace(line, inserted_code)
filename_to_code_map[filename] = result
return result

View file

@ -1,7 +1,5 @@
#version 330
#INSERT camera_uniform_declarations.glsl
uniform sampler2D Texture;
in vec3 point;
@ -13,7 +11,6 @@ out float v_opacity;
// Analog of import for manim only
#INSERT get_gl_Position.glsl
#INSERT position_point_into_frame.glsl
void main(){
v_im_coords = im_coords;

View file

@ -1,6 +0,0 @@
uniform vec2 frame_shape;
uniform vec2 pixel_shape;
uniform vec3 camera_offset;
uniform mat3 camera_rotation;
uniform float is_fixed_in_frame;
uniform float focal_distance;

View file

@ -1,3 +1,9 @@
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_data){
float alpha = clamp((value - min_val) / (max_val - min_val), 0.0, 1.0);
int disc_alpha = min(int(alpha * 8), 7);
@ -9,14 +15,16 @@ vec3 float_to_color(float value, float min_val, float max_val, vec3[9] colormap_
}
vec4 add_light(vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow){
vec4 add_light(
vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow
){
if(reflectiveness == 0.0 && gloss == 0.0 && shadow == 0.0) return color;
vec4 result = color;
@ -42,22 +50,19 @@ vec4 add_light(vec4 color,
// Darken
result.rgb = mix(result.rgb, vec3(0.0), -light_to_normal * shadow);
}
// float darkening = mix(1, max(light_to_normal, 0), shadow);
// return vec4(
// darkening * mix(color.rgb, vec3(1.0), shine),
// color.a
// );
return result;
}
vec4 finalize_color(vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow){
vec4 finalize_color(
vec4 color,
vec3 point,
vec3 unit_normal,
vec3 light_coords,
vec3 cam_coords,
float reflectiveness,
float gloss,
float shadow
){
///// INSERT COLOR FUNCTION HERE /////
// The line above may be replaced by arbitrary code snippets, as per
// the method Mobject.set_color_by_code

View file

@ -1,31 +1,38 @@
// Assumes the following uniforms exist in the surrounding context:
// uniform vec2 frame_shape;
// uniform float focal_distance;
// uniform float is_fixed_in_frame;
uniform float is_fixed_in_frame;
uniform vec3 camera_offset;
uniform mat3 camera_rotation;
uniform vec2 frame_shape;
uniform float focal_distance;
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
float perspective_scale_factor(float z, float focal_distance){
return max(0.0, focal_distance / (focal_distance - z));
vec4 get_gl_Position(vec3 point){
vec2 shape;
if(bool(is_fixed_in_frame)) shape = DEFAULT_FRAME_SHAPE;
else shape = frame_shape;
vec4 result = vec4(point, 1.0);
result.x *= 2.0 / shape.x;
result.y *= 2.0 / shape.y;
result.z /= focal_distance;
result.w = 1.0 - result.z;
// Flip and scale to prevent premature clipping
result.z *= -0.1;
return result;
}
vec4 get_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0);
if(!bool(is_fixed_in_frame)){
result.x *= 2.0 / frame_shape.x;
result.y *= 2.0 / frame_shape.y;
float psf = perspective_scale_factor(result.z, focal_distance);
if (psf > 0){
result.xy *= psf;
// TODO, what's the better way to do this?
// This is to keep vertices too far out of frame from getting cut.
result.z *= 0.01;
}
} else{
result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x;
result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y;
vec3 rotate_point_into_frame(vec3 point){
if(bool(is_fixed_in_frame)){
return point;
}
result.z *= -1;
return result;
}
return camera_rotation * point;
}
vec3 position_point_into_frame(vec3 point){
if(bool(is_fixed_in_frame)){
return point;
}
return rotate_point_into_frame(point - camera_offset);
}

View file

@ -1,22 +1,20 @@
vec3 get_unit_normal(in vec3[3] points){
vec3 get_unit_normal(vec3 p0, vec3 p1, vec3 p2){
float tol = 1e-6;
vec3 v1 = normalize(points[1] - points[0]);
vec3 v2 = normalize(points[2] - points[1]);
vec3 v1 = normalize(p1 - p0);
vec3 v2 = normalize(p2 - p1);
vec3 cp = cross(v1, v2);
float cp_norm = length(cp);
if(cp_norm < tol){
// Three points form a line, so find a normal vector
// to that line in the plane shared with the z-axis
vec3 k_hat = vec3(0.0, 0.0, 1.0);
vec3 comb = v1 + v2;
vec3 new_cp = cross(cross(comb, k_hat), comb);
float new_cp_norm = length(new_cp);
if(new_cp_norm < tol){
// We only come here if all three points line up
// on the z-axis.
return vec3(0.0, -1.0, 0.0);
}
return new_cp / new_cp_norm;
}
return cp / cp_norm;
if(cp_norm > tol) return cp / cp_norm;
// Otherwise, three pionts form a line, so find
// a normal vector to that line in the plane shared
// with the z-axis
vec3 comb = v1 + v2;
cp = cross(cross(comb, vec3(0.0, 0.0, 1.0)), comb);
cp_norm = length(cp);
if(cp_norm > tol) return cp / cp_norm;
// Otherwise, the points line up with the z-axis.
return vec3(0.0, -1.0, 0.0);
}

View file

@ -0,0 +1,88 @@
float cross2d(vec2 v, vec2 w){
return v.x * w.y - w.x * v.y;
}
vec2 complex_div(vec2 v, vec2 w){
return vec2(dot(v, w), cross2d(w, v)) / dot(w, w);
}
vec2 xs_on_clean_parabola(vec2 b0, vec2 b1, vec2 b2){
/*
Given three control points for a quadratic bezier,
this returns the two values (x0, x2) such that the
section of the parabola y = x^2 between those values
is isometric to the given quadratic bezier.
Adapated from https://github.com/raphlinus/raphlinus.github.io/blob/master/_posts/2019-12-23-flatten-quadbez.md
*/
vec2 dd = normalize(2 * b1 - b0 - b2);
float u0 = dot(b1 - b0, dd);
float u2 = dot(b2 - b1, dd);
float cp = cross2d(b2 - b0, dd);
return vec2(u0 / cp, u2 / cp);
}
mat3 map_point_pairs(vec2 src0, vec2 src1, vec2 dest0, vec2 dest1){
/*
Returns an orthogonal matrix which will map
src0 onto dest0 and src1 onto dest1.
*/
mat3 shift1 = mat3(
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
-src0.x, -src0.y, 1.0
);
mat3 shift2 = mat3(
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
dest0.x, dest0.y, 1.0
);
// Compute complex division dest_vect / src_vect to determine rotation
vec2 complex_rot = complex_div(dest1 - dest0, src1 - src0);
mat3 rotate = mat3(
complex_rot.x, complex_rot.y, 0.0,
-complex_rot.y, complex_rot.x, 0.0,
0.0, 0.0, 1.0
);
return shift2 * rotate * shift1;
}
mat3 get_xy_to_uv(vec2 b0, vec2 b1, vec2 b2, float temp_is_linear, out float is_linear){
/*
Returns a matrix for an affine transformation which maps a set of quadratic
bezier controls points into a new coordinate system such that the bezier curve
coincides with y = x^2, or in the case of a linear curve, it's mapped to the x-axis.
*/
vec2 dest0;
vec2 dest1;
is_linear = temp_is_linear;
// Portions of the parabola y = x^2 where abs(x) exceeds
// this value are treated as straight lines.
float thresh = 2.0;
if (!bool(is_linear)){
vec2 xs = xs_on_clean_parabola(b0, b1, b2);
float x0 = xs.x;
float x2 = xs.y;
if((x0 > thresh && x2 > thresh) || (x0 < -thresh && x2 < -thresh)){
is_linear = 1.0;
}else{
dest0 = vec2(x0, x0 * x0);
dest1 = vec2(x2, x2 * x2);
}
}
// Check if is_linear status changed above
if (bool(is_linear)){
dest0 = vec2(0, 0);
dest1 = vec2(1, 0);
}
return map_point_pairs(b0, b2, dest0, dest1);
}

View file

@ -1,19 +0,0 @@
// Assumes the following uniforms exist in the surrounding context:
// uniform float is_fixed_in_frame;
// uniform vec3 camera_offset;
// uniform mat3 camera_rotation;
vec3 rotate_point_into_frame(vec3 point){
if(bool(is_fixed_in_frame)){
return point;
}
return camera_rotation * point;
}
vec3 position_point_into_frame(vec3 point){
if(bool(is_fixed_in_frame)){
return point;
}
return rotate_point_into_frame(point - camera_offset);
}

View file

@ -1,107 +0,0 @@
// Must be inserted in a context with a definition for modify_distance_for_endpoints
// All of this is with respect to a curve that's been rotated/scaled
// so that b0 = (0, 0) and b1 = (1, 0). That is, b2 entirely
// determines the shape of the curve
vec2 bezier(float t, vec2 b2){
// Quick returns for the 0 and 1 cases
if (t == 0) return vec2(0, 0);
else if (t == 1) return b2;
// Everything else
return vec2(
2 * t * (1 - t) + b2.x * t*t,
b2.y * t * t
);
}
float cube_root(float x){
return sign(x) * pow(abs(x), 1.0 / 3.0);
}
int cubic_solve(float a, float b, float c, float d, out float roots[3]){
// Normalize so a = 1
b = b / a;
c = c / a;
d = d / a;
float p = c - b*b / 3.0;
float q = b * (2.0*b*b - 9.0*c) / 27.0 + d;
float p3 = p*p*p;
float disc = q*q + 4.0*p3 / 27.0;
float offset = -b / 3.0;
if(disc >= 0.0){
float z = sqrt(disc);
float u = (-q + z) / 2.0;
float v = (-q - z) / 2.0;
u = cube_root(u);
v = cube_root(v);
roots[0] = offset + u + v;
return 1;
}
float u = sqrt(-p / 3.0);
float v = acos(-sqrt( -27.0 / p3) * q / 2.0) / 3.0;
float m = cos(v);
float n = sin(v) * 1.732050808;
float all_roots[3] = float[3](
offset + u * (n - m),
offset - u * (n + m),
offset + u * (m + m)
);
// Only accept roots with a positive derivative
int n_valid_roots = 0;
for(int i = 0; i < 3; i++){
float r = all_roots[i];
if(3*r*r + 2*b*r + c > 0){
roots[n_valid_roots] = r;
n_valid_roots++;
}
}
return n_valid_roots;
}
float dist_to_line(vec2 p, vec2 b2){
float t = clamp(p.x / b2.x, 0, 1);
float dist;
if(t == 0) dist = length(p);
else if(t == 1) dist = distance(p, b2);
else dist = abs(p.y);
return modify_distance_for_endpoints(p, dist, t);
}
float dist_to_point_on_curve(vec2 p, float t, vec2 b2){
t = clamp(t, 0, 1);
return modify_distance_for_endpoints(
p, length(p - bezier(t, b2)), t
);
}
float min_dist_to_curve(vec2 p, vec2 b2, float degree){
// Check if curve is really a a line
if(degree == 1) return dist_to_line(p, b2);
// Try finding the exact sdf by solving the equation
// (d/dt) dist^2(t) = 0, which amount to the following
// cubic.
float xm2 = uv_b2.x - 2.0;
float y = uv_b2.y;
float a = xm2*xm2 + y*y;
float b = 3 * xm2;
float c = -(p.x*xm2 + p.y*y) + 2;
float d = -p.x;
float roots[3];
int n = cubic_solve(a, b, c, d, roots);
// At most 2 roots will have been populated.
float d0 = dist_to_point_on_curve(p, roots[0], b2);
if(n == 1) return d0;
float d1 = dist_to_point_on_curve(p, roots[1], b2);
return min(d0, d1);
}

View file

@ -1,92 +0,0 @@
float cross2d(vec2 v, vec2 w){
return v.x * w.y - w.x * v.y;
}
mat3 get_xy_to_uv(vec2 b0, vec2 b1){
mat3 shift = mat3(
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
-b0.x, -b0.y, 1.0
);
float sf = length(b1 - b0);
vec2 I = (b1 - b0) / sf;
vec2 J = vec2(-I.y, I.x);
mat3 rotate = mat3(
I.x, J.x, 0.0,
I.y, J.y, 0.0,
0.0, 0.0, 1.0
);
return (1.0 / sf) * rotate * shift;
}
// Orthogonal matrix to convert to a uv space defined so that
// b0 goes to [0, 0] and b1 goes to [1, 0]
mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){
mat4 shift = mat4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
-b0.x, -b0.y, -b0.z, 1
);
float scale_factor = length(b1 - b0);
vec3 I = (b1 - b0) / scale_factor;
vec3 K = unit_normal;
vec3 J = cross(K, I);
// Transpose (hence inverse) of matrix taking
// i-hat to I, k-hat to unit_normal, and j-hat to their cross
mat4 rotate = mat4(
I.x, J.x, K.x, 0.0,
I.y, J.y, K.y, 0.0,
I.z, J.z, K.z, 0.0,
0.0, 0.0, 0.0, 1.0
);
return (1.0 / scale_factor) * rotate * shift;
}
// Returns 0 for null curve, 1 for linear, 2 for quadratic.
// Populates new_points with bezier control points for the curve,
// which for quadratics will be the same, but for linear and null
// might change. The idea is to inform the caller of the degree,
// while also passing tangency information in the linear case.
// float get_reduced_control_points(vec3 b0, vec3 b1, vec3 b2, out vec3 new_points[3]){
float get_reduced_control_points(in vec3 points[3], out vec3 new_points[3]){
float length_threshold = 1e-8;
float angle_threshold = 1e-3;
vec3 p0 = points[0];
vec3 p1 = points[1];
vec3 p2 = points[2];
vec3 v01 = (p1 - p0);
vec3 v12 = (p2 - p1);
float dot_prod = clamp(dot(normalize(v01), normalize(v12)), -1, 1);
bool aligned = acos(dot_prod) < angle_threshold;
bool distinct_01 = length(v01) > length_threshold; // v01 is considered nonzero
bool distinct_12 = length(v12) > length_threshold; // v12 is considered nonzero
int n_uniques = int(distinct_01) + int(distinct_12);
bool quadratic = (n_uniques == 2) && !aligned;
bool linear = (n_uniques == 1) || ((n_uniques == 2) && aligned);
bool constant = (n_uniques == 0);
if(quadratic){
new_points[0] = p0;
new_points[1] = p1;
new_points[2] = p2;
return 2.0;
}else if(linear){
new_points[0] = p0;
new_points[1] = 0.5 * (p0 + p2);
new_points[2] = p2;
return 1.0;
}else{
new_points[0] = p0;
new_points[1] = p0;
new_points[2] = p0;
return 0.0;
}
}

View file

@ -0,0 +1,19 @@
mat3 rotationMatrix(vec3 axis, float angle) {
axis = normalize(axis);
float s = sin(angle);
float c = cos(angle);
float oc = 1.0 - c;
float ax = axis.x;
float ay = axis.y;
float az = axis.z;
return mat3(
oc * ax * ax + c, oc * ax * ay - az * s, oc * az * ax + ay * s,
oc * ax * ay + az * s, oc * ay * ay + c, oc * ay * az - ax * s,
oc * az * ax - ay * s, oc * ay * az + ax * s, oc * az * az + c
);
}
vec3 rotate(vec3 vect, float angle, vec3 axis){
return rotationMatrix(axis, angle) * vect;
}

View file

@ -1,12 +1,5 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
uniform vec2 parameter;
uniform float opacity;
uniform float n_steps;

View file

@ -1,14 +1,11 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
out vec3 xyz_coords;
uniform float scale_factor;
uniform vec3 offset;
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
void main(){

View file

@ -1,12 +1,5 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
uniform vec4 color0;
uniform vec4 color1;
uniform vec4 color2;

View file

@ -1,14 +1,11 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
out vec3 xyz_coords;
uniform float scale_factor;
uniform vec3 offset;
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
void main(){

View file

@ -1,59 +1,29 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec4 color;
in float fill_all; // Either 0 or 1
in float uv_anti_alias_width;
in vec3 xyz_coords;
in float orientation;
in vec2 uv_coords;
in vec2 uv_b2;
in float bezier_degree;
in float is_linear;
out vec4 frag_color;
// Needed for quadratic_bezier_distance insertion below
float modify_distance_for_endpoints(vec2 p, float dist, float t){
return dist;
}
float sdf(float x0, float y0){
if(bool(is_linear)) return abs(y0);
#INSERT quadratic_bezier_distance.glsl
float Fxy = y0 - x0 * x0;
if(orientation * Fxy >= 0) return 0.0;
float sdf(){
if(bezier_degree < 2){
return abs(uv_coords[1]);
}
float u2 = uv_b2.x;
float v2 = uv_b2.y;
// For really flat curves, just take the distance to x-axis
if(abs(v2 / u2) < 0.1 * uv_anti_alias_width){
return abs(uv_coords[1]);
}
// This converts uv_coords to yet another space where the bezier points sit on
// (0, 0), (1/2, 0) and (1, 1), so that the curve can be expressed implicityly
// as y = x^2.
mat2 to_simple_space = mat2(
v2, 0,
2 - u2, 4 * v2
);
vec2 p = to_simple_space * uv_coords;
// Sign takes care of whether we should be filling the inside or outside of curve.
float sgn = orientation * sign(v2);
float Fp = (p.x * p.x - p.y);
if(sgn * Fp <= 0){
return 0.0;
}else{
return min_dist_to_curve(uv_coords, uv_b2, bezier_degree);
}
return abs(Fxy) / sqrt(1 + 4 * x0 * x0);
}
void main() {
if (color.a == 0) discard;
frag_color = color;
if (fill_all == 1.0) return;
frag_color.a *= smoothstep(1, 0, sdf() / uv_anti_alias_width);
if (bool(fill_all)) return;
float dist = sdf(uv_coords.x, uv_coords.y);
frag_color.a *= smoothstep(1, 0, dist / uv_anti_alias_width);
}

View file

@ -4,20 +4,9 @@ layout (triangles) in;
layout (triangle_strip, max_vertices = 5) out;
uniform float anti_alias_width;
// Needed for get_gl_Position
uniform vec2 frame_shape;
uniform vec2 pixel_shape;
uniform float focal_distance;
uniform float is_fixed_in_frame;
// Needed for finalize_color
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
in vec3 bp[3];
in vec3 verts[3];
in float v_orientation[3];
in vec4 v_color[3];
in float v_vert_index[3];
@ -26,24 +15,21 @@ out vec4 color;
out float fill_all;
out float uv_anti_alias_width;
out vec3 xyz_coords;
out float orientation;
// uv space is where b0 = (0, 0), b1 = (1, 0), and transform is orthogonal
// uv space is where the curve coincides with y = x^2
out vec2 uv_coords;
out vec2 uv_b2;
out float bezier_degree;
out float is_linear;
vec3 unit_normal;
const float ANGLE_THRESHOLD = 1e-3;
// Analog of import for manim only
#INSERT quadratic_bezier_geometry_functions.glsl
#INSERT get_gl_Position.glsl
#INSERT get_unit_normal.glsl
#INSERT get_xy_to_uv.glsl
#INSERT finalize_color.glsl
void emit_vertex_wrapper(vec3 point, int index){
void emit_vertex_wrapper(vec3 point, int index, vec3 unit_normal){
color = finalize_color(
v_color[index],
point,
@ -54,71 +40,62 @@ void emit_vertex_wrapper(vec3 point, int index){
gloss,
shadow
);
xyz_coords = point;
gl_Position = get_gl_Position(xyz_coords);
gl_Position = get_gl_Position(point);
EmitVertex();
}
void emit_simple_triangle(){
void emit_simple_triangle(vec3 unit_normal){
for(int i = 0; i < 3; i++){
emit_vertex_wrapper(bp[i], i);
emit_vertex_wrapper(verts[i], i, unit_normal);
}
EndPrimitive();
}
void emit_pentagon(vec3[3] points, vec3 normal){
vec3 p0 = points[0];
vec3 p1 = points[1];
vec3 p2 = points[2];
// Tangent vectors
vec3 t01 = normalize(p1 - p0);
vec3 t12 = normalize(p2 - p1);
// Vectors perpendicular to the curve in the plane of the curve pointing outside the curve
vec3 p0_perp = cross(t01, normal);
vec3 p2_perp = cross(t12, normal);
void emit_pentagon(
// Triangle vertices
vec3 p0,
vec3 p1,
vec3 p2,
// Unit tangent vector
vec3 t01,
vec3 t12,
vec3 unit_normal
){
// Vectors perpendicular to the curve in the plane of the curve
// pointing outside the curve
vec3 p0_perp = cross(t01, unit_normal);
vec3 p2_perp = cross(t12, unit_normal);
float angle = acos(clamp(dot(t01, t12), -1, 1));
is_linear = float(angle < ANGLE_THRESHOLD);
bool fill_inside = orientation > 0.0;
float aaw = anti_alias_width * frame_shape.y / pixel_shape.y;
vec3 corners[5];
if(bezier_degree == 1.0){
// For straight lines, buff out in both directions
corners = vec3[5](
p0 + aaw * p0_perp,
p0 - aaw * p0_perp,
p1 + 0.5 * aaw * (p0_perp + p2_perp),
p2 - aaw * p2_perp,
p2 + aaw * p2_perp
);
} else if(fill_inside){
// If curved, and filling insight, just buff out away interior
corners = vec3[5](
p0 + aaw * p0_perp,
p0,
p1 + 0.5 * aaw * (p0_perp + p2_perp),
p2,
p2 + aaw * p2_perp
);
}else{
corners = vec3[5](
p0,
p0 - aaw * p0_perp,
p1,
p2 - aaw * p2_perp,
p2
);
vec3 corners[5] = vec3[5](p0, p0, p1, p2, p2);
if(fill_inside || bool(is_linear)){
// Add buffer outside the curve
corners[0] += aaw * p0_perp;
corners[2] += 0.5 * aaw * (p0_perp + p2_perp);
corners[4] += aaw * p2_perp;
}
if(!fill_inside || bool(is_linear)){
// Add buffer inside the curve
corners[1] -= aaw * p0_perp;
corners[3] -= aaw * p2_perp;
}
mat4 xyz_to_uv = get_xyz_to_uv(p0, p1, normal);
uv_b2 = (xyz_to_uv * vec4(p2, 1)).xy;
uv_anti_alias_width = aaw / length(p1 - p0);
// Compute xy_to_uv matrix, and potentially re-evaluate bezier degree
mat3 xy_to_uv = get_xy_to_uv(p0.xy, p1.xy, p2.xy, is_linear, is_linear);
uv_anti_alias_width = aaw * length(xy_to_uv[0].xy);
for(int i = 0; i < 5; i++){
vec3 corner = corners[i];
uv_coords = (xyz_to_uv * vec4(corner, 1)).xy;
int j = int(sign(i - 1) + 1); // Maps i = [0, 1, 2, 3, 4] onto j = [0, 0, 1, 2, 2]
emit_vertex_wrapper(corner, j);
vec3 corner = corners[i];
uv_coords = (xy_to_uv * vec3(corner.xy, 1.0)).xy;
emit_vertex_wrapper(corner, j, unit_normal);
}
EndPrimitive();
}
@ -131,19 +108,24 @@ void main(){
(v_vert_index[2] - v_vert_index[1]) != 1.0
);
if(fill_all == 1.0){
emit_simple_triangle();
vec3 p0 = verts[0];
vec3 p1 = verts[1];
vec3 p2 = verts[2];
vec3 t01 = p1 - p0;
vec3 t12 = p2 - p1;
vec3 unit_normal = normalize(cross(t01, t12));
if(bool(fill_all)){
emit_simple_triangle(unit_normal);
return;
}
orientation = v_orientation[1];
vec3 new_bp[3];
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), new_bp);
unit_normal = get_unit_normal(new_bp);
orientation = v_orientation[0];
if(bezier_degree >= 1){
emit_pentagon(new_bp, unit_normal);
}
// Don't emit any vertices for bezier_degree 0
emit_pentagon(
p0, p1, p2,
normalize(t01),
normalize(t12),
unit_normal
);
}

View file

@ -1,22 +1,20 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
in float orientation;
in vec4 color;
in float vert_index;
out vec3 bp; // Bezier control point
out vec3 verts; // Bezier control point
out float v_orientation;
out vec4 v_color;
out float v_vert_index;
// Analog of import for manim only
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
void main(){
bp = position_point_into_frame(point);
verts = position_point_into_frame(point);
v_orientation = orientation;
v_color = color;
v_vert_index = vert_index;

View file

@ -1,92 +1,58 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec2 uv_coords;
in vec2 uv_b2;
in float uv_stroke_width;
in vec4 color;
in float uv_anti_alias_width;
in vec4 color;
in float has_prev;
in float has_next;
in float bevel_start;
in float bevel_end;
in float angle_from_prev;
in float angle_to_next;
in float bezier_degree;
in float is_linear;
out vec4 frag_color;
const float QUICK_DIST_WIDTH = 0.2;
float cross2d(vec2 v, vec2 w){
return v.x * w.y - w.x * v.y;
}
// Distance from (x0, y0) to the curve y = x^2
float dist_to_curve(float x0, float y0){
// In the linear case, the curve will have
// been set to equal the x axis
if(bool(is_linear)) return y0;
float modify_distance_for_endpoints(vec2 p, float dist, float t){
float buff = 0.5 * uv_stroke_width - uv_anti_alias_width;
// Check the beginning of the curve
if(t == 0){
// Clip the start
if(has_prev == 0) return max(dist, -p.x + buff);
// Bevel start
if(bevel_start == 1){
float a = angle_from_prev;
mat2 rot = mat2(
cos(a), sin(a),
-sin(a), cos(a)
);
// Dist for intersection of two lines
float bevel_d = max(abs(p.y), abs((rot * p).y));
// Dist for union of this intersection with the real curve
// intersected with radius 2 away from curve to smooth out
// really sharp corners
return max(min(dist, bevel_d), dist / 2);
}
// Otherwise, start will be rounded off
}else if(t == 1){
// Check the end of the curve
// TODO, too much code repetition
vec2 v21 = (bezier_degree == 2) ? vec2(1, 0) - uv_b2 : vec2(-1, 0);
float len_v21 = length(v21);
if(len_v21 == 0){
v21 = -uv_b2;
len_v21 = length(v21);
}
float perp_dist = dot(p - uv_b2, v21) / len_v21;
if(has_next == 0) return max(dist, -perp_dist + buff);
// Bevel end
if(bevel_end == 1){
float a = -angle_to_next;
mat2 rot = mat2(
cos(a), sin(a),
-sin(a), cos(a)
);
vec2 v21_unit = v21 / length(v21);
float bevel_d = max(
abs(cross2d(p - uv_b2, v21_unit)),
abs(cross2d((rot * (p - uv_b2)), v21_unit))
);
return max(min(dist, bevel_d), dist / 2);
}
// Otherwise, end will be rounded off
if(uv_stroke_width < QUICK_DIST_WIDTH){
// This is a quick approximation for computing
// the distance to the curve.
// Evaluate F(x, y) = y - x^2
// divide by its gradient's magnitude
return (y0 - x0 * x0) / sqrt(1 + 4 * x0 * x0);
}
return dist;
// Otherwise, solve for the minimal distance.
// The distance squared between (x0, y0) and a point (x, x^2) looks like
//
// (x0 - x)^2 + (y0 - x^2)^2 = x^4 + (1 - 2y0)x^2 - 2x0 * x + (x0^2 + y0^2)
//
// Setting the derivative equal to zero (and rescaling) looks like
//
// x^3 + (0.5 - y0) * x - 0.5 * x0 = 0
//
// Use two rounds of Newton's method
float x = x0;
float p = (0.5 - y0);
float q = -0.5 * x0;
for(int i = 0; i < 2; i++){
float fx = x * x * x + p * x + q;
float dfx = 3 * x * x + p;
x = x - fx / dfx;
}
return distance(uv_coords, vec2(x, x * x));
}
#INSERT quadratic_bezier_distance.glsl
void main() {
if (uv_stroke_width == 0) discard;
float dist_to_curve = min_dist_to_curve(uv_coords, uv_b2, bezier_degree);
// An sdf for the region around the curve we wish to color.
float signed_dist = abs(dist_to_curve) - 0.5 * uv_stroke_width;
// Compute sdf for the region around the curve we wish to color.
float dist = dist_to_curve(uv_coords.x, uv_coords.y);
float signed_dist = abs(dist) - 0.5 * uv_stroke_width;
frag_color = color;
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width);

View file

@ -1,104 +1,58 @@
#version 330
layout (triangles) in;
layout (triangle_strip, max_vertices = 5) out;
// Needed for get_gl_Position
uniform vec2 frame_shape;
uniform vec2 pixel_shape;
uniform float focal_distance;
uniform float is_fixed_in_frame;
layout (triangle_strip, max_vertices = 6) out;
uniform float anti_alias_width;
uniform float flat_stroke;
//Needed for lighting
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform vec2 pixel_shape;
uniform float joint_type;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
in vec3 bp[3];
in vec3 prev_bp[3];
in vec3 next_bp[3];
in vec3 verts[3];
in vec4 v_color[3];
in float v_joint_angle[3];
in float v_stroke_width[3];
in vec4 v_color[3];
in float v_vert_index[3];
out vec4 color;
out float uv_stroke_width;
out float uv_anti_alias_width;
out float has_prev;
out float has_next;
out float bevel_start;
out float bevel_end;
out float angle_from_prev;
out float angle_to_next;
out float bezier_degree;
out float is_linear;
out vec2 uv_coords;
out vec2 uv_b2;
vec3 unit_normal;
// Codes for joint types
const float AUTO_JOINT = 0;
const float ROUND_JOINT = 1;
const float BEVEL_JOINT = 2;
const float MITER_JOINT = 3;
const int NO_JOINT = 0;
const int AUTO_JOINT = 1;
const int BEVEL_JOINT = 2;
const int MITER_JOINT = 3;
const float PI = 3.141592653;
const float ANGLE_THRESHOLD = 1e-3;
#INSERT quadratic_bezier_geometry_functions.glsl
#INSERT get_gl_Position.glsl
#INSERT get_unit_normal.glsl
#INSERT get_xy_to_uv.glsl
#INSERT finalize_color.glsl
void flatten_points(in vec3[3] points, out vec2[3] flat_points){
for(int i = 0; i < 3; i++){
float sf = perspective_scale_factor(points[i].z, focal_distance);
flat_points[i] = sf * points[i].xy;
}
}
float angle_between_vectors(vec2 v1, vec2 v2){
float v1_norm = length(v1);
float v2_norm = length(v2);
if(v1_norm == 0 || v2_norm == 0) return 0.0;
float dp = dot(v1, v2) / (v1_norm * v2_norm);
float angle = acos(clamp(dp, -1.0, 1.0));
float sn = sign(cross2d(v1, v2));
return sn * angle;
}
bool find_intersection(vec2 p0, vec2 v0, vec2 p1, vec2 v1, out vec2 intersection){
// Find the intersection of a line passing through
// p0 in the direction v0 and one passing through p1 in
// the direction p1.
// That is, find a solutoin to p0 + v0 * t = p1 + v1 * s
float det = -v0.x * v1.y + v1.x * v0.y;
if(det == 0) return false;
float t = cross2d(p0 - p1, v1) / det;
intersection = p0 + v0 * t;
return true;
}
void create_joint(float angle, vec2 unit_tan, float buff,
vec2 static_c0, out vec2 changing_c0,
vec2 static_c1, out vec2 changing_c1){
void create_joint(
float angle,
vec3 unit_tan,
float buff,
vec3 static_c0,
out vec3 changing_c0,
vec3 static_c1,
out vec3 changing_c1
){
float shift;
if(abs(angle) < 1e-3){
// if(abs(angle) < ANGLE_THRESHOLD || abs(angle) > 0.99 * PI || int(joint_type) == NO_JOINT){
if(abs(angle) < ANGLE_THRESHOLD || int(joint_type) == NO_JOINT){
// No joint
shift = 0;
}else if(joint_type == MITER_JOINT){
}else if(int(joint_type) == MITER_JOINT){
shift = buff * (-1.0 - cos(angle)) / sin(angle);
}else{
// For a Bevel joint
@ -108,160 +62,121 @@ void create_joint(float angle, vec2 unit_tan, float buff,
changing_c1 = static_c1 + shift * unit_tan;
}
// This function is responsible for finding the corners of
// a bounding region around the bezier curve, which can be
// emitted as a triangle fan
int get_corners(vec2 controls[3], int degree, float stroke_widths[3], out vec2 corners[5]){
vec2 p0 = controls[0];
vec2 p1 = controls[1];
vec2 p2 = controls[2];
// emitted as a triangle fan, with vertices vaguely close
// to control points so that the passage of vert data to
// frag shaders is most natural.
void get_corners(
// Control points for a bezier curve
vec3 p0,
vec3 p1,
vec3 p2,
// Unit tangent vectors at p0 and p2
vec3 v01,
vec3 v12,
float stroke_width0,
float stroke_width2,
// Unit normal to the whole curve
vec3 normal,
// Anti-alias width
float aaw,
float angle_from_prev,
float angle_to_next,
out vec3 corners[6]
){
// Unit vectors for directions between control points
vec2 v10 = normalize(p0 - p1);
vec2 v12 = normalize(p2 - p1);
vec2 v01 = -v10;
vec2 v21 = -v12;
float buff0 = 0.5 * stroke_width0 + aaw;
float buff2 = 0.5 * stroke_width2 + aaw;
vec2 p0_perp = vec2(-v01.y, v01.x); // Pointing to the left of the curve from p0
vec2 p2_perp = vec2(-v12.y, v12.x); // Pointing to the left of the curve from p2
// Add correction for sharp angles to prevent weird bevel effects (Needed?)
float thresh = 5 * PI / 6;
if(angle_from_prev > thresh) buff0 *= 2 * sin(angle_from_prev);
if(angle_to_next > thresh) buff2 *= 2 * sin(angle_to_next);
// aaw is the added width given around the polygon for antialiasing.
// In case the normal is faced away from (0, 0, 1), the vector to the
// camera, this is scaled up.
float aaw = anti_alias_width * frame_shape.y / pixel_shape.y;
float buff0 = 0.5 * stroke_widths[0] + aaw;
float buff2 = 0.5 * stroke_widths[2] + aaw;
float aaw0 = (1 - has_prev) * aaw;
float aaw2 = (1 - has_next) * aaw;
// Perpendicular vectors to the left of the curve
vec3 p0_perp = buff0 * normalize(cross(normal, v01));
vec3 p2_perp = buff2 * normalize(cross(normal, v12));
vec3 p1_perp = 0.5 * (p0_perp + p2_perp);
vec2 c0 = p0 - buff0 * p0_perp + aaw0 * v10;
vec2 c1 = p0 + buff0 * p0_perp + aaw0 * v10;
vec2 c2 = p2 + buff2 * p2_perp + aaw2 * v12;
vec2 c3 = p2 - buff2 * p2_perp + aaw2 * v12;
// The order of corners should be for a triangle_strip.
vec3 c0 = p0 + p0_perp;
vec3 c1 = p0 - p0_perp;
vec3 c2 = p1 + p1_perp;
vec3 c3 = p1 - p1_perp;
vec3 c4 = p2 + p2_perp;
vec3 c5 = p2 - p2_perp;
float orientation = dot(normal, cross(v01, v12));
// Move the inner middle control point to make
// room for the curve
if(orientation > 0.0) c2 = 0.5 * (c0 + c4);
else if(orientation < 0.0) c3 = 0.5 * (c1 + c5);
// Account for previous and next control points
if(has_prev > 0) create_joint(angle_from_prev, v01, buff0, c0, c0, c1, c1);
if(has_next > 0) create_joint(angle_to_next, v21, buff2, c3, c3, c2, c2);
create_joint(angle_from_prev, v01, buff0, c1, c1, c0, c0);
create_joint(angle_to_next, -v12, buff2, c5, c5, c4, c4);
// Linear case is the simplest
if(degree == 1){
// The order of corners should be for a triangle_strip. Last entry is a dummy
corners = vec2[5](c0, c1, c3, c2, vec2(0.0));
return 4;
}
// Otherwise, form a pentagon around the curve
float orientation = sign(cross2d(v01, v12)); // Positive for ccw curves
if(orientation > 0) corners = vec2[5](c0, c1, p1, c2, c3);
else corners = vec2[5](c1, c0, p1, c3, c2);
// Replace corner[2] with convex hull point accounting for stroke width
find_intersection(corners[0], v01, corners[4], v21, corners[2]);
return 5;
}
void set_adjascent_info(vec2 c0, vec2 tangent,
int degree,
vec2 adj[3],
out float bevel,
out float angle
){
bool linear_adj = (angle_between_vectors(adj[1] - adj[0], adj[2] - adj[1]) < 1e-3);
angle = angle_between_vectors(c0 - adj[1], tangent);
// Decide on joint type
bool one_linear = (degree == 1 || linear_adj);
bool should_bevel = (
(joint_type == AUTO_JOINT && one_linear) ||
joint_type == BEVEL_JOINT
);
bevel = should_bevel ? 1.0 : 0.0;
}
void find_joint_info(vec2 controls[3], vec2 prev[3], vec2 next[3], int degree){
float tol = 1e-6;
// Made as floats not bools so they can be passed to the frag shader
has_prev = float(distance(prev[2], controls[0]) < tol);
has_next = float(distance(next[0], controls[2]) < tol);
if(bool(has_prev)){
vec2 tangent = controls[1] - controls[0];
set_adjascent_info(
controls[0], tangent, degree, prev,
bevel_start, angle_from_prev
);
}
if(bool(has_next)){
vec2 tangent = controls[1] - controls[2];
set_adjascent_info(
controls[2], tangent, degree, next,
bevel_end, angle_to_next
);
angle_to_next *= -1;
}
corners = vec3[6](c0, c1, c2, c3, c4, c5);
}
void main() {
// Convert control points to a standard form if they are linear or null
vec3 controls[3];
vec3 prev[3];
vec3 next[3];
unit_normal = get_unit_normal(controls);
bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), controls);
if(bezier_degree == 0.0) return; // Null curve
int degree = int(bezier_degree);
get_reduced_control_points(vec3[3](prev_bp[0], prev_bp[1], prev_bp[2]), prev);
get_reduced_control_points(vec3[3](next_bp[0], next_bp[1], next_bp[2]), next);
// We use the triangle strip primative, but
// actually only need every other strip element
if (int(v_vert_index[0]) % 2 == 1) return;
// Curves are marked as eneded when the handle after
// the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return;
// Adjust stroke width based on distance from the camera
float scaled_strokes[3];
for(int i = 0; i < 3; i++){
float sf = perspective_scale_factor(controls[i].z, focal_distance);
if(bool(flat_stroke)){
vec3 to_cam = normalize(vec3(0.0, 0.0, focal_distance) - controls[i]);
sf *= abs(dot(unit_normal, to_cam));
}
scaled_strokes[i] = v_stroke_width[i] * sf;
// TODO, track true unit normal globally (probably as a uniform)
vec3 unit_normal = vec3(0.0, 0.0, 1.0);
if(bool(flat_stroke)){
unit_normal = camera_rotation * vec3(0.0, 0.0, 1.0);
}
// Control points are projected to the xy plane before drawing, which in turn
// gets tranlated to a uv plane. The z-coordinate information will be remembered
// by what's sent out to gl_Position, and by how it affects the lighting and stroke width
vec2 flat_controls[3];
vec2 flat_prev[3];
vec2 flat_next[3];
flatten_points(controls, flat_controls);
flatten_points(prev, flat_prev);
flatten_points(next, flat_next);
vec3 p0 = verts[0];
vec3 p1 = verts[1];
vec3 p2 = verts[2];
vec3 v01 = normalize(p1 - p0);
vec3 v12 = normalize(p2 - p1);
find_joint_info(flat_controls, flat_prev, flat_next, degree);
float angle = acos(clamp(dot(v01, v12), -1, 1));
is_linear = float(abs(angle) < ANGLE_THRESHOLD);
// Corners of a bounding region around curve
vec2 corners[5];
int n_corners = get_corners(flat_controls, degree, scaled_strokes, corners);
// If the curve is flat, put the middle control in the midpoint
if (bool(is_linear)) p1 = 0.5 * (p0 + p2);
int index_map[5] = int[5](0, 0, 1, 2, 2);
if(n_corners == 4) index_map[2] = 2;
// We want to change the coordinates to a space where the curve
// coincides with y = x^2, between some values x0 and x2. Or, in
// the case of a linear curve (bezier degree 1), just put it on
// the segment from (0, 0) to (1, 0)
mat3 xy_to_uv = get_xy_to_uv(p0.xy, p1.xy, p2.xy, is_linear, is_linear);
// Find uv conversion matrix
mat3 xy_to_uv = get_xy_to_uv(flat_controls[0], flat_controls[1]);
float scale_factor = length(flat_controls[1] - flat_controls[0]);
uv_anti_alias_width = anti_alias_width * frame_shape.y / pixel_shape.y / scale_factor;
uv_b2 = (xy_to_uv * vec3(flat_controls[2], 1.0)).xy;
float uv_scale_factor = length(xy_to_uv[0].xy);
float scaled_aaw = anti_alias_width * (frame_shape.y / pixel_shape.y);
uv_anti_alias_width = uv_scale_factor * scaled_aaw;
vec3 corners[6];
get_corners(
p0, p1, p2, v01, v12,
v_stroke_width[0],
v_stroke_width[2],
unit_normal,
scaled_aaw,
v_joint_angle[0],
v_joint_angle[2],
corners
);
// Emit each corner
for(int i = 0; i < n_corners; i++){
uv_coords = (xy_to_uv * vec3(corners[i], 1.0)).xy;
uv_stroke_width = scaled_strokes[index_map[i]] / scale_factor;
// Apply some lighting to the color before sending out.
// vec3 xyz_coords = vec3(corners[i], controls[index_map[i]].z);
vec3 xyz_coords = vec3(corners[i], controls[index_map[i]].z);
for(int i = 0; i < 6; i++){
int vert_index = i / 2;
uv_coords = (xy_to_uv * vec3(corners[i].xy, 1)).xy;
uv_stroke_width = uv_scale_factor * v_stroke_width[vert_index];
color = finalize_color(
v_color[index_map[i]],
xyz_coords,
v_color[vert_index],
corners[i],
unit_normal,
light_source_position,
camera_position,
@ -269,10 +184,7 @@ void main() {
gloss,
shadow
);
gl_Position = vec4(
get_gl_Position(vec3(corners[i], 0.0)).xy,
get_gl_Position(controls[index_map[i]]).zw
);
gl_Position = get_gl_Position(corners[i]);
EmitVertex();
}
EndPrimitive();

View file

@ -1,31 +1,28 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
in vec3 prev_point;
in vec3 next_point;
in float joint_angle;
in float stroke_width;
in vec4 color;
// Bezier control point
out vec3 bp;
out vec3 prev_bp;
out vec3 next_bp;
out vec3 verts;
out float v_joint_angle;
out float v_stroke_width;
out vec4 v_color;
out float v_vert_index;
const float STROKE_WIDTH_CONVERSION = 0.01;
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
void main(){
bp = position_point_into_frame(point);
prev_bp = position_point_into_frame(prev_point);
next_bp = position_point_into_frame(next_point);
verts = position_point_into_frame(point);
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0;
v_joint_angle = joint_angle;
v_color = color;
v_vert_index = gl_VertexID;
}

View file

@ -1,12 +1,8 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
// Analog of import for manim only
#INSERT get_gl_Position.glsl
#INSERT position_point_into_frame.glsl
void main(){
gl_Position = get_gl_Position(position_point_into_frame(point));

View file

@ -1,29 +1,8 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
in vec3 xyz_coords;
in vec3 v_normal;
in vec4 v_color;
out vec4 frag_color;
#INSERT finalize_color.glsl
void main() {
frag_color = finalize_color(
v_color,
xyz_coords,
normalize(v_normal),
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);
frag_color = v_color;
}

View file

@ -1,6 +1,6 @@
#version 330
#INSERT camera_uniform_declarations.glsl
uniform vec4 clip_plane;
in vec3 point;
in vec3 du_point;
@ -11,13 +11,28 @@ out vec3 xyz_coords;
out vec3 v_normal;
out vec4 v_color;
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
#INSERT get_rotated_surface_unit_normal_vector.glsl
#INSERT finalize_color.glsl
void main(){
xyz_coords = position_point_into_frame(point);
v_normal = get_rotated_surface_unit_normal_vector(point, du_point, dv_point);
v_color = color;
gl_Position = get_gl_Position(xyz_coords);
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
}
v_color = finalize_color(
color,
xyz_coords,
v_normal,
light_source_position,
camera_position,
reflectiveness,
gloss,
shadow
);
}

View file

@ -3,12 +3,6 @@
uniform sampler2D LightTexture;
uniform sampler2D DarkTexture;
uniform float num_textures;
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float focal_distance;
in vec3 xyz_coords;
in vec3 v_normal;

View file

@ -1,7 +1,5 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
@ -13,7 +11,6 @@ out vec3 v_normal;
out vec2 v_im_coords;
out float v_opacity;
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
#INSERT get_rotated_surface_unit_normal_vector.glsl

View file

@ -1,18 +1,13 @@
#version 330
uniform vec3 light_source_position;
uniform vec3 camera_position;
uniform float reflectiveness;
uniform float gloss;
uniform float shadow;
uniform float anti_alias_width;
uniform float focal_distance;
uniform float glow_factor;
in vec4 color;
in float radius;
in vec2 center;
in vec2 point;
in float scaled_aaw;
out vec4 frag_color;
@ -22,7 +17,7 @@ void main() {
vec2 diff = point - center;
float dist = length(diff);
float signed_dist = dist - radius;
if (signed_dist > 0.5 * anti_alias_width){
if (signed_dist > 0.5 * scaled_aaw){
discard;
}
frag_color = color;
@ -43,5 +38,5 @@ void main() {
frag_color.a *= pow(1 - dist / radius, glow_factor);
}
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / anti_alias_width);
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / scaled_aaw);
}

View file

@ -3,11 +3,8 @@
layout (points) in;
layout (triangle_strip, max_vertices = 4) out;
// Needed for get_gl_Position
uniform vec2 frame_shape;
uniform float focal_distance;
uniform float is_fixed_in_frame;
uniform float anti_alias_width;
uniform vec2 pixel_shape;
in vec3 v_point[1];
in float v_radius[1];
@ -17,6 +14,7 @@ out vec4 color;
out float radius;
out vec2 center;
out vec2 point;
out float scaled_aaw;
#INSERT get_gl_Position.glsl
@ -25,8 +23,9 @@ void main() {
radius = v_radius[0];
center = v_point[0].xy;
scaled_aaw = (frame_shape.y / pixel_shape.y);
radius = v_radius[0] / max(1.0 - v_point[0].z / focal_distance / frame_shape.y, 0.0);
float rpa = radius + anti_alias_width;
float rpa = radius + scaled_aaw;
for(int i = 0; i < 4; i++){
// To account for perspective

View file

@ -1,7 +1,5 @@
#version 330
#INSERT camera_uniform_declarations.glsl
in vec3 point;
in float radius;
in vec4 color;
@ -10,7 +8,7 @@ out vec3 v_point;
out float v_radius;
out vec4 v_color;
#INSERT position_point_into_frame.glsl
#INSERT get_gl_Position.glsl
void main(){
v_point = position_point_into_frame(point);

View file

@ -36,3 +36,4 @@ if TYPE_CHECKING:
Vect2Array = Annotated[FloatArray, Literal["N", 2]]
Vect3Array = Annotated[FloatArray, Literal["N", 3]]
Vect4Array = Annotated[FloatArray, Literal["N", 4]]
VectNArray = Annotated[FloatArray, Literal["N", "M"]]

View file

@ -12,8 +12,8 @@ from manimlib.utils.space_ops import midpoint
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Sequence, TypeVar
from manimlib.typing import VectN, FloatArray
from typing import Callable, Sequence, TypeVar, Tuple
from manimlib.typing import VectN, FloatArray, VectNArray
Scalable = TypeVar("Scalable", float, FloatArray)
@ -22,7 +22,7 @@ CLOSED_THRESHOLD = 0.001
def bezier(
points: Sequence[Scalable]
points: Sequence[Scalable] | VectNArray
) -> Callable[[float], Scalable]:
if len(points) == 0:
raise Exception("bezier cannot be calld on an empty list")
@ -69,10 +69,10 @@ def partial_bezier_points(
# Shortened version of partial_bezier_points just for quadratics,
# since this is called a fair amount
def partial_quadratic_bezier_points(
points: Sequence[Scalable],
points: Sequence[VectN] | VectNArray,
a: float,
b: float
) -> list[Scalable]:
) -> list[VectN]:
if a == 1:
return 3 * [points[-1]]
@ -202,7 +202,7 @@ def get_smooth_quadratic_bezier_handle_points(
def get_smooth_cubic_bezier_handle_points(
points: Sequence[VectN]
points: Sequence[VectN] | VectNArray
) -> tuple[FloatArray, FloatArray]:
points = np.array(points)
num_handles = len(points) - 1
@ -292,7 +292,7 @@ def get_quadratic_approximation_of_cubic(
h0: FloatArray,
h1: FloatArray,
a1: FloatArray
) -> np.ndarray:
) -> FloatArray:
a0 = np.array(a0, ndmin=2)
h0 = np.array(h0, ndmin=2)
h1 = np.array(h1, ndmin=2)
@ -350,13 +350,12 @@ def get_quadratic_approximation_of_cubic(
i1 = find_intersection(a1, T1, mid, Tm)
m, n = np.shape(a0)
result = np.zeros((6 * m, n))
result[0::6] = a0
result[1::6] = i0
result[2::6] = mid
result[3::6] = mid
result[4::6] = i1
result[5::6] = a1
result = np.zeros((5 * m, n))
result[0::5] = a0
result[1::5] = i0
result[2::5] = mid
result[3::5] = i1
result[4::5] = a1
return result

View file

@ -126,6 +126,10 @@ def make_even(
)
def arrays_match(arr1: np.ndarray, arr2: np.ndarray) -> bool:
return arr1.shape == arr2.shape and (arr1 == arr2).all()
def hash_obj(obj: object) -> int:
if isinstance(obj, dict):
return hash(tuple(sorted([

View file

@ -51,6 +51,12 @@ def clip(a: float, min_a: float, max_a: float) -> float:
return a
def arr_clip(arr: np.ndarray, min_a: float, max_a: float) -> np.ndarray:
arr[arr < min_a] = min_a
arr[arr > max_a] = max_a
return arr
def fdiv(a: Scalable, b: Scalable, zero_over_zero_value: Scalable | None = None) -> Scalable:
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)

View file

@ -22,12 +22,20 @@ if TYPE_CHECKING:
from manimlib.typing import Vect2, Vect3, Vect4, VectN, Matrix3x3, Vect3Array, Vect2Array
def cross(v1: Vect3 | List[float], v2: Vect3 | List[float]) -> Vect3:
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 cross(v1: Vect3 | List[float], v2: Vect3 | List[float]) -> Vect3 | Vect3Array:
is2d = isinstance(v1, np.ndarray) and len(v1.shape) == 2
if is2d:
x1, y1, z1 = v1[:, 0], v1[:, 1], v1[:, 2]
x2, y2, z2 = v2[:, 0], v2[:, 1], v2[:, 2]
else:
x1, y1, z1 = v1
x2, y2, z2 = v2
result = np.array([
y1 * z2 - z1 * y2,
z1 * x2 - x1 * z2,
x1 * y2 - y1 * x2,
])
return result.T if is2d else result
def get_norm(vect: VectN | List[float]) -> float:
@ -134,15 +142,16 @@ def rotation_about_z(angle: float) -> Matrix3x3:
def rotation_between_vectors(v1: Vect3, v2: Vect3) -> Matrix3x3:
if np.isclose(v1, v2).all():
atol = 1e-8
if get_norm(v1 - v2) < atol:
return np.identity(3)
axis = np.cross(v1, v2)
if np.isclose(axis, [0, 0, 0]).all():
axis = cross(v1, v2)
if get_norm(axis) < atol:
# v1 and v2 align
axis = np.cross(v1, RIGHT)
if np.isclose(axis, [0, 0, 0]).all():
axis = cross(v1, RIGHT)
if get_norm(axis) < atol:
# v1 and v2 _and_ RIGHT all align
axis = np.cross(v1, UP)
axis = cross(v1, UP)
return rotation_matrix(
angle=angle_between_vectors(v1, v2),
axis=axis,
@ -157,7 +166,7 @@ def angle_of_vector(vector: Vect2 | Vect3) -> float:
"""
Returns polar coordinate theta when vector is project on xy plane
"""
return np.angle(complex(*vector[:2]))
return math.atan2(vector[1], vector[0])
def angle_between_vectors(v1: VectN, v2: VectN) -> float:
@ -184,8 +193,7 @@ def normalize_along_axis(
) -> np.ndarray:
norms = np.sqrt((array * array).sum(axis))
norms[norms == 0] = 1
buffed_norms = np.repeat(norms, array.shape[axis]).reshape(array.shape)
return array / buffed_norms
return (array.T / norms).T
def get_unit_normal(
@ -271,41 +279,61 @@ def line_intersection(
def find_intersection(
p0: Vect3,
v0: Vect3,
p1: Vect3,
v1: Vect3,
threshold: float = 1e-5
p0: Vect3 | Vect3Array,
v0: Vect3 | Vect3Array,
p1: Vect3 | Vect3Array,
v1: Vect3 | Vect3Array,
threshold: float = 1e-5,
) -> Vect3:
"""
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
d = len(p0.shape)
if d == 1:
is_3d = any(arr[2] for arr in (p0, v0, p1, v1))
else:
is_3d = any(z for arr in (p0, v0, p1, v1) for z in arr.T[2])
if not is_3d:
numer = np.array(cross2d(v1, p1 - p0))
denom = np.array(cross2d(v1, v0))
else:
cp1 = cross(v1, p1 - p0)
cp2 = cross(v1, v0)
numer = np.array((cp1 * cp1).sum(d - 1))
denom = np.array((cp1 * cp2).sum(d - 1))
denom[abs(denom) < threshold] = np.inf
ratio = numer / denom
ratio = np.repeat(ratio, n).reshape((m, n))
result = p0 + ratio * v0
if m == 1:
return result[0]
return result
return p0 + (ratio * v0.T).T
def line_intersects_path(
start: Vect2 | Vect3,
end: Vect2 | Vect3,
path: Vect2Array | Vect3Array,
) -> bool:
"""
Tests whether the line (start, end) intersects
a polygonal path defined by its vertices
"""
n = len(path) - 1
p1 = np.empty((n, 2))
q1 = np.empty((n, 2))
p1[:] = start[:2]
q1[:] = end[:2]
p2 = path[:-1, :2]
q2 = path[1:, :2]
v1 = q1 - p1
v2 = q2 - p2
mis1 = cross2d(v1, p2 - p1) * cross2d(v1, q2 - p1) < 0
mis2 = cross2d(v2, p1 - p2) * cross2d(v2, q1 - p2) < 0
return bool((mis1 * mis2).any())
def get_closest_point_on_line(a: VectN, b: VectN, p: VectN) -> VectN:

View file

@ -18,8 +18,7 @@ def num_tex_symbols(tex: str) -> int:
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
for s in ["begin", "end", "phantom"]
)
for tup in re.findall(pattern, tex):
tex = tex.replace("".join(tup), " ")
tex = re.sub(pattern, "", tex)
# Progressively count the symbols associated with certain tex commands,
# and remove those commands from the string, adding the number of symbols