mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
commit
f296dd8df5
20 changed files with 324 additions and 219 deletions
|
@ -73,6 +73,7 @@ from manimlib.utils.iterables import *
|
|||
from manimlib.utils.paths import *
|
||||
from manimlib.utils.rate_functions import *
|
||||
from manimlib.utils.simple_functions import *
|
||||
from manimlib.utils.shaders import *
|
||||
from manimlib.utils.sounds import *
|
||||
from manimlib.utils.space_ops import *
|
||||
from manimlib.utils.tex import *
|
||||
|
|
|
@ -6,6 +6,8 @@ from manimlib.animation.animation import Animation
|
|||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Group
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.bezier import integer_interpolate
|
||||
from manimlib.utils.bezier import interpolate
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
|
@ -14,7 +16,7 @@ from manimlib.utils.simple_functions import clip
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from typing import Callable, Optional
|
||||
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.scene.scene import Scene
|
||||
|
@ -28,8 +30,8 @@ class AnimationGroup(Animation):
|
|||
*animations: Animation | _AnimationBuilder,
|
||||
run_time: float = -1, # If negative, default to sum of inputed animation runtimes
|
||||
lag_ratio: float = 0.0,
|
||||
group: Mobject | None = None,
|
||||
group_type: type = Group,
|
||||
group: Optional[Mobject] = None,
|
||||
group_type: Optional[type] = None,
|
||||
**kwargs
|
||||
):
|
||||
self.animations = [prepare_animation(anim) for anim in animations]
|
||||
|
@ -37,11 +39,15 @@ class AnimationGroup(Animation):
|
|||
self.max_end_time = max((awt[2] for awt in self.anims_with_timings), default=0)
|
||||
self.run_time = self.max_end_time if run_time < 0 else run_time
|
||||
self.lag_ratio = lag_ratio
|
||||
self.group = group
|
||||
if self.group is None:
|
||||
self.group = group_type(*remove_list_redundancies(
|
||||
[anim.mobject for anim in self.animations]
|
||||
))
|
||||
mobs = remove_list_redundancies([a.mobject for a in self.animations])
|
||||
if group is not None:
|
||||
self.group = group
|
||||
if group_type is not None:
|
||||
self.group = group_type(*mobs)
|
||||
elif all(isinstance(anim.mobject, VMobject) for anim in animations):
|
||||
self.group = VGroup(*mobs)
|
||||
else:
|
||||
self.group = Group(*mobs)
|
||||
|
||||
super().__init__(
|
||||
self.group,
|
||||
|
|
|
@ -100,7 +100,8 @@ class DrawBorderThenFill(Animation):
|
|||
def begin(self) -> None:
|
||||
# Trigger triangulation calculation
|
||||
for submob in self.mobject.get_family():
|
||||
submob.get_triangulation()
|
||||
if not submob._use_winding_fill:
|
||||
submob.get_triangulation()
|
||||
|
||||
self.outline = self.get_outline()
|
||||
super().begin()
|
||||
|
|
|
@ -265,7 +265,7 @@ class FlashAround(VShowPassingFlash):
|
|||
**kwargs
|
||||
):
|
||||
path = self.get_path(mobject, buff)
|
||||
if mobject.is_fixed_in_frame:
|
||||
if mobject.is_fixed_in_frame():
|
||||
path.fix_in_frame()
|
||||
path.insert_n_curves(n_inserted_curves)
|
||||
path.set_points(path.get_points_without_null_curves())
|
||||
|
|
|
@ -32,7 +32,6 @@ class TransformMatchingParts(AnimationGroup):
|
|||
mismatch_animation: type = Transform,
|
||||
run_time: float = 2,
|
||||
lag_ratio: float = 0,
|
||||
group_type: type = Group,
|
||||
**kwargs,
|
||||
):
|
||||
self.source = source
|
||||
|
@ -76,7 +75,6 @@ class TransformMatchingParts(AnimationGroup):
|
|||
*self.anims,
|
||||
run_time=run_time,
|
||||
lag_ratio=lag_ratio,
|
||||
group_type=group_type,
|
||||
)
|
||||
|
||||
def add_transform(
|
||||
|
@ -151,7 +149,6 @@ class TransformMatchingStrings(TransformMatchingParts):
|
|||
super().__init__(
|
||||
source, target,
|
||||
matched_pairs=matched_pairs,
|
||||
group_type=VGroup,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ from manimlib.utils.color import color_to_rgba
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
from manimlib.window import Window
|
||||
|
||||
|
@ -24,8 +25,8 @@ if TYPE_CHECKING:
|
|||
class Camera(object):
|
||||
def __init__(
|
||||
self,
|
||||
window: Window | None = None,
|
||||
background_image: str | None = None,
|
||||
window: Optional[Window] = None,
|
||||
background_image: Optional[str] = None,
|
||||
frame_config: dict = dict(),
|
||||
pixel_width: int = DEFAULT_PIXEL_WIDTH,
|
||||
pixel_height: int = DEFAULT_PIXEL_HEIGHT,
|
||||
|
@ -62,31 +63,38 @@ class Camera(object):
|
|||
))
|
||||
self.uniforms = dict()
|
||||
self.init_frame(**frame_config)
|
||||
self.init_context(window)
|
||||
self.init_context()
|
||||
self.init_fbo()
|
||||
self.init_light_source()
|
||||
|
||||
def init_frame(self, **config) -> None:
|
||||
self.frame = CameraFrame(**config)
|
||||
|
||||
def init_context(self, window: Window | None = None) -> None:
|
||||
self.window = window
|
||||
if window is None:
|
||||
def init_context(self) -> None:
|
||||
if self.window is None:
|
||||
self.ctx = moderngl.create_standalone_context()
|
||||
self.fbo = self.get_fbo(self.samples)
|
||||
else:
|
||||
self.ctx = window.ctx
|
||||
self.window_fbo = self.ctx.detect_framebuffer()
|
||||
self.fbo_for_files = self.get_fbo(self.samples)
|
||||
self.fbo = self.window_fbo
|
||||
|
||||
self.fbo.use()
|
||||
self.ctx = self.window.ctx
|
||||
|
||||
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
|
||||
def init_fbo(self) -> None:
|
||||
# This is the buffer used when writing to a video/image file
|
||||
self.fbo_for_files = self.get_fbo(self.samples)
|
||||
|
||||
# This is the frame buffer we'll draw into when emitting frames
|
||||
self.draw_fbo = self.get_fbo(samples=0)
|
||||
|
||||
if self.window is None:
|
||||
self.window_fbo = None
|
||||
self.fbo = self.fbo_for_files
|
||||
else:
|
||||
self.window_fbo = self.ctx.detect_framebuffer()
|
||||
self.fbo = self.window_fbo
|
||||
|
||||
self.fbo.use()
|
||||
|
||||
def init_light_source(self) -> None:
|
||||
self.light_source = Point(self.light_source_position)
|
||||
|
||||
|
@ -210,6 +218,7 @@ class Camera(object):
|
|||
|
||||
# Rendering
|
||||
def capture(self, *mobjects: Mobject) -> None:
|
||||
self.clear()
|
||||
self.refresh_uniforms()
|
||||
self.fbo.use()
|
||||
for mobject in mobjects:
|
||||
|
|
|
@ -685,7 +685,6 @@ class Arrow(Line):
|
|||
self.width_to_tip_len = width_to_tip_len
|
||||
self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio
|
||||
self.max_width_to_length_ratio = max_width_to_length_ratio
|
||||
self.max_stroke_width = stroke_width
|
||||
super().__init__(
|
||||
start, end,
|
||||
stroke_color=stroke_color,
|
||||
|
@ -715,22 +714,21 @@ 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])
|
||||
# Dumb that this is needed
|
||||
self.start_new_path(self.point_from_proportion(1 - 1e-5))
|
||||
self.add_line_to(prev_end)
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def create_tip_with_stroke_width(self):
|
||||
if not self.has_points():
|
||||
if self.get_num_points() < 3:
|
||||
return self
|
||||
width = min(
|
||||
self.max_stroke_width,
|
||||
tip_width = self.tip_width_ratio * min(
|
||||
float(self.get_stroke_width()),
|
||||
self.max_width_to_length_ratio * self.get_length(),
|
||||
)
|
||||
widths_array = np.full(self.get_num_points(), width)
|
||||
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)
|
||||
self.data['stroke_width'][:-3] = self.data['stroke_width'][0]
|
||||
self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3)
|
||||
return self
|
||||
|
||||
def reset_tip(self):
|
||||
|
@ -747,13 +745,14 @@ class Arrow(Line):
|
|||
*args, **kwargs
|
||||
):
|
||||
super().set_stroke(color=color, width=width, *args, **kwargs)
|
||||
if isinstance(width, numbers.Number):
|
||||
self.max_stroke_width = width
|
||||
self.create_tip_with_stroke_width()
|
||||
if self.has_points():
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor: float):
|
||||
return self.reset_tip()
|
||||
if scale_factor != 1.0:
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
||||
|
||||
class FillArrow(Line):
|
||||
|
|
|
@ -50,7 +50,7 @@ from typing import TYPE_CHECKING
|
|||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Union, Tuple
|
||||
import numpy.typing as npt
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, UniformDict
|
||||
from moderngl.context import Context
|
||||
|
||||
TimeBasedUpdater = Callable[["Mobject", float], "Mobject" | None]
|
||||
|
@ -88,7 +88,7 @@ class Mobject(object):
|
|||
self.opacity = opacity
|
||||
self.shading = shading
|
||||
self.texture_paths = texture_paths
|
||||
self.is_fixed_in_frame = is_fixed_in_frame
|
||||
self._is_fixed_in_frame = is_fixed_in_frame
|
||||
self.depth_test = depth_test
|
||||
|
||||
# Internal state
|
||||
|
@ -131,8 +131,8 @@ class Mobject(object):
|
|||
self.data = np.zeros(length, dtype=self.shader_dtype)
|
||||
|
||||
def init_uniforms(self):
|
||||
self.uniforms: dict[str, float | np.ndarray] = {
|
||||
"is_fixed_in_frame": float(self.is_fixed_in_frame),
|
||||
self.uniforms: UniformDict = {
|
||||
"is_fixed_in_frame": float(self._is_fixed_in_frame),
|
||||
"shading": np.array(self.shading, dtype=float),
|
||||
}
|
||||
|
||||
|
@ -408,14 +408,15 @@ class Mobject(object):
|
|||
self.assemble_family()
|
||||
return self
|
||||
|
||||
def remove(self, *mobjects: Mobject, reassemble: bool = True):
|
||||
for mobject in mobjects:
|
||||
if mobject in self.submobjects:
|
||||
self.submobjects.remove(mobject)
|
||||
if self in mobject.parents:
|
||||
mobject.parents.remove(self)
|
||||
if reassemble:
|
||||
self.assemble_family()
|
||||
def remove(self, *to_remove: Mobject, reassemble: bool = True):
|
||||
for parent in self.get_family():
|
||||
for child in to_remove:
|
||||
if child in parent.submobjects:
|
||||
parent.submobjects.remove(child)
|
||||
if parent in child.parents:
|
||||
child.parents.remove(parent)
|
||||
if reassemble:
|
||||
parent.assemble_family()
|
||||
return self
|
||||
|
||||
def add_to_back(self, *mobjects: Mobject):
|
||||
|
@ -591,19 +592,22 @@ class Mobject(object):
|
|||
result._shaders_initialized = False
|
||||
result._data_has_changed = True
|
||||
|
||||
@stash_mobject_pointers
|
||||
def copy(self, deep: bool = False):
|
||||
if deep:
|
||||
return self.deepcopy()
|
||||
|
||||
result = copy.copy(self)
|
||||
|
||||
# The line above is only a shallow copy, so the internal
|
||||
# data which are numpyu arrays or other mobjects still
|
||||
result.parents = []
|
||||
result.target = None
|
||||
result.saved_state = None
|
||||
|
||||
# copy.copy is only a shallow copy, so the internal
|
||||
# data which are numpy arrays or other mobjects still
|
||||
# need to be further copied.
|
||||
result.data = self.data.copy()
|
||||
result.uniforms = {
|
||||
key: np.array(value)
|
||||
key: value.copy() if isinstance(value, np.ndarray) else value
|
||||
for key, value in self.uniforms.items()
|
||||
}
|
||||
|
||||
|
@ -622,7 +626,7 @@ class Mobject(object):
|
|||
result._data_has_changed = True
|
||||
|
||||
family = self.get_family()
|
||||
for attr, value in list(self.__dict__.items()):
|
||||
for attr, value in self.__dict__.items():
|
||||
if isinstance(value, Mobject) and value is not self:
|
||||
if value in family:
|
||||
setattr(result, attr, result.family[self.family.index(value)])
|
||||
|
@ -1765,20 +1769,26 @@ class Mobject(object):
|
|||
self.locked_data_keys = set(keys)
|
||||
|
||||
def lock_matching_data(self, mobject1: Mobject, mobject2: Mobject):
|
||||
for sm, sm1, sm2 in zip(self.get_family(), mobject1.get_family(), mobject2.get_family()):
|
||||
if sm.data.dtype == sm1.data.dtype == sm2.data.dtype:
|
||||
names = sm.data.dtype.names
|
||||
sm.lock_data(filter(
|
||||
lambda name: arrays_match(sm1.data[name], sm2.data[name]),
|
||||
names,
|
||||
))
|
||||
sm.const_data_keys = set(filter(
|
||||
lambda name: all(
|
||||
array_is_constant(mob.data[name])
|
||||
for mob in (sm, sm1, sm2)
|
||||
),
|
||||
names
|
||||
))
|
||||
tuples = zip(
|
||||
self.get_family(),
|
||||
mobject1.get_family(),
|
||||
mobject2.get_family(),
|
||||
)
|
||||
for sm, sm1, sm2 in tuples:
|
||||
if not sm.data.dtype == sm1.data.dtype == sm2.data.dtype:
|
||||
continue
|
||||
names = sm.data.dtype.names
|
||||
sm.lock_data(filter(
|
||||
lambda name: arrays_match(sm1.data[name], sm2.data[name]),
|
||||
names,
|
||||
))
|
||||
sm.const_data_keys = set(filter(
|
||||
lambda name: all(
|
||||
array_is_constant(mob.data[name])
|
||||
for mob in (sm, sm1, sm2)
|
||||
),
|
||||
names
|
||||
))
|
||||
|
||||
return self
|
||||
|
||||
|
@ -1799,17 +1809,19 @@ class Mobject(object):
|
|||
return wrapper
|
||||
|
||||
@affects_shader_info_id
|
||||
def fix_in_frame(self):
|
||||
self.uniforms["is_fixed_in_frame"] = 1.0
|
||||
self.is_fixed_in_frame = True
|
||||
def fix_in_frame(self, recurse: bool = True):
|
||||
for mob in self.get_family(recurse):
|
||||
mob.uniforms["is_fixed_in_frame"] = 1.0
|
||||
return self
|
||||
|
||||
@affects_shader_info_id
|
||||
def unfix_from_frame(self):
|
||||
self.uniforms["is_fixed_in_frame"] = 0.0
|
||||
self.is_fixed_in_frame = False
|
||||
return self
|
||||
|
||||
def is_fixed_in_frame(self) -> bool:
|
||||
return bool(self.uniforms["is_fixed_in_frame"])
|
||||
|
||||
@affects_shader_info_id
|
||||
def apply_depth_test(self):
|
||||
self.depth_test = True
|
||||
|
@ -1894,7 +1906,7 @@ class Mobject(object):
|
|||
|
||||
self.shader_wrapper.vert_data = self.get_shader_data()
|
||||
self.shader_wrapper.vert_indices = self.get_shader_vert_indices()
|
||||
self.shader_wrapper.uniforms.update(self.get_uniforms())
|
||||
self.shader_wrapper.update_program_uniforms(self.get_uniforms())
|
||||
self.shader_wrapper.depth_test = self.depth_test
|
||||
return self.shader_wrapper
|
||||
|
||||
|
@ -1931,8 +1943,9 @@ class Mobject(object):
|
|||
shader_wrapper.generate_vao()
|
||||
self._data_has_changed = False
|
||||
for shader_wrapper in self.shader_wrappers:
|
||||
shader_wrapper.uniforms.update(self.get_uniforms())
|
||||
shader_wrapper.uniforms.update(camera_uniforms)
|
||||
shader_wrapper.depth_test = self.depth_test
|
||||
shader_wrapper.update_program_uniforms(self.get_uniforms())
|
||||
shader_wrapper.update_program_uniforms(camera_uniforms, universal=True)
|
||||
shader_wrapper.pre_render()
|
||||
shader_wrapper.render()
|
||||
|
||||
|
@ -2048,7 +2061,7 @@ class Group(Mobject):
|
|||
raise Exception("All submobjects must be of type Mobject")
|
||||
Mobject.__init__(self, **kwargs)
|
||||
self.add(*mobjects)
|
||||
if any(m.is_fixed_in_frame for m in mobjects):
|
||||
if any(m.is_fixed_in_frame() for m in mobjects):
|
||||
self.fix_in_frame()
|
||||
|
||||
def __add__(self, other: Mobject | Group):
|
||||
|
|
|
@ -74,6 +74,7 @@ class SVGMobject(VMobject):
|
|||
|
||||
super().__init__(**kwargs )
|
||||
self.init_svg_mobject()
|
||||
self.ensure_positive_orientation()
|
||||
|
||||
# Rather than passing style into super().__init__
|
||||
# do it after svg has been taken in
|
||||
|
@ -320,10 +321,6 @@ class VMobjectFromSVGPath(VMobject):
|
|||
self.set_points(self.get_points_without_null_curves())
|
||||
# So triangulation doesn't get messed up
|
||||
self.subdivide_intersections()
|
||||
# Always default to orienting outward, account
|
||||
# for the fact that this will get flipped in SVG.__init__
|
||||
if self.get_unit_normal()[2] > 0:
|
||||
self.reverse_points()
|
||||
# Save for future use
|
||||
PATH_TO_POINTS[path_string] = self.get_points().copy()
|
||||
else:
|
||||
|
|
|
@ -195,6 +195,7 @@ class Surface(Mobject):
|
|||
).reshape(shape)
|
||||
return points.reshape((nu * nv, *resolution[2:]))
|
||||
|
||||
@Mobject.affects_data
|
||||
def sort_faces_back_to_front(self, vect: Vect3 = OUT):
|
||||
tri_is = self.triangle_indices
|
||||
points = self.get_points()
|
||||
|
|
|
@ -62,8 +62,10 @@ class VMobject(Mobject):
|
|||
('joint_product', np.float32, (4,)),
|
||||
('fill_rgba', np.float32, (4,)),
|
||||
('base_point', np.float32, (3,)),
|
||||
('unit_normal', np.float32, (3,)),
|
||||
('fill_border_width', np.float32, (1,)),
|
||||
])
|
||||
fill_data_names = ['point', 'fill_rgba', 'base_point']
|
||||
fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal']
|
||||
stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product']
|
||||
|
||||
fill_render_primitive: int = moderngl.TRIANGLE_STRIP
|
||||
|
@ -91,6 +93,7 @@ class VMobject(Mobject):
|
|||
use_simple_quadratic_approx: bool = False,
|
||||
# Measured in pixel widths
|
||||
anti_alias_width: float = 1.0,
|
||||
fill_border_width: float = 0.5,
|
||||
use_winding_fill: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
|
@ -106,6 +109,7 @@ class VMobject(Mobject):
|
|||
self.flat_stroke = flat_stroke
|
||||
self.use_simple_quadratic_approx = use_simple_quadratic_approx
|
||||
self.anti_alias_width = anti_alias_width
|
||||
self.fill_border_width = fill_border_width
|
||||
self._use_winding_fill = use_winding_fill
|
||||
|
||||
self.needs_new_triangulation = True
|
||||
|
@ -163,6 +167,7 @@ class VMobject(Mobject):
|
|||
self.set_fill(
|
||||
color=self.fill_color,
|
||||
opacity=self.fill_opacity,
|
||||
border_width=self.fill_border_width,
|
||||
)
|
||||
self.set_stroke(
|
||||
color=self.stroke_color,
|
||||
|
@ -194,9 +199,13 @@ class VMobject(Mobject):
|
|||
self,
|
||||
color: ManimColor | Iterable[ManimColor] = None,
|
||||
opacity: float | Iterable[float] | None = None,
|
||||
border_width: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
|
||||
if border_width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.data["fill_border_width"] = border_width
|
||||
return self
|
||||
|
||||
def set_stroke(
|
||||
|
@ -669,6 +678,8 @@ class VMobject(Mobject):
|
|||
|
||||
def append_vectorized_mobject(self, vmobject: VMobject):
|
||||
self.add_subpath(vmobject.get_points())
|
||||
n = vmobject.get_num_points()
|
||||
self.data[-n:] = vmobject.data
|
||||
return self
|
||||
|
||||
#
|
||||
|
@ -825,8 +836,31 @@ class VMobject(Mobject):
|
|||
points[1] - points[0],
|
||||
points[2] - points[1],
|
||||
)
|
||||
self.data["unit_normal"][:] = normal
|
||||
return normal
|
||||
|
||||
def refresh_unit_normal(self):
|
||||
self.get_unit_normal()
|
||||
return self
|
||||
|
||||
def rotate(
|
||||
self,
|
||||
angle: float,
|
||||
axis: Vect3 = OUT,
|
||||
about_point: Vect3 | None = None,
|
||||
**kwargs
|
||||
):
|
||||
super().rotate(angle, axis, about_point, **kwargs)
|
||||
for mob in self.get_family():
|
||||
mob.refresh_unit_normal()
|
||||
return self
|
||||
|
||||
def ensure_positive_orientation(self, recurse=True):
|
||||
for mob in self.get_family(recurse):
|
||||
if mob.get_unit_normal()[2] < 0:
|
||||
mob.reverse_points()
|
||||
return self
|
||||
|
||||
# Alignment
|
||||
def align_points(self, vmobject: VMobject):
|
||||
winding = self._use_winding_fill and vmobject._use_winding_fill
|
||||
|
@ -1056,8 +1090,6 @@ class VMobject(Mobject):
|
|||
inner_tri_indices = iti[~(null1 | null2).repeat(3)]
|
||||
|
||||
ovi = self.get_outer_vert_indices()
|
||||
# Flip outer triangles with negative orientation
|
||||
ovi[0::3][concave_parts], ovi[2::3][concave_parts] = ovi[2::3][concave_parts], ovi[0::3][concave_parts]
|
||||
tri_indices = np.hstack([ovi, inner_tri_indices])
|
||||
self.triangulation = tri_indices
|
||||
self.needs_new_triangulation = False
|
||||
|
@ -1140,6 +1172,7 @@ class VMobject(Mobject):
|
|||
self.refresh_triangulation()
|
||||
if refresh_joints:
|
||||
self.get_joint_products(refresh=True)
|
||||
self.get_unit_normal()
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
|
@ -1149,14 +1182,15 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def reverse_points(self):
|
||||
def reverse_points(self, recurse: bool = True):
|
||||
# This will reset which anchors are
|
||||
# considered path ends
|
||||
for mob in self.get_family():
|
||||
for mob in self.get_family(recurse):
|
||||
if not mob.has_points():
|
||||
continue
|
||||
inner_ends = mob.get_subpath_end_indices()[:-1]
|
||||
mob.data["point"][inner_ends + 1] = mob.data["point"][inner_ends + 2]
|
||||
mob.data["unit_normal"] *= -1
|
||||
super().reverse_points()
|
||||
return self
|
||||
|
||||
|
@ -1235,24 +1269,33 @@ class VMobject(Mobject):
|
|||
|
||||
# Build up data lists
|
||||
fill_datas = []
|
||||
fill_border_datas = []
|
||||
fill_indices = []
|
||||
stroke_datas = []
|
||||
back_stroke_data = []
|
||||
back_stroke_datas = []
|
||||
for submob in family:
|
||||
if submob.has_fill():
|
||||
submob.data["base_point"][:] = submob.data["point"][0]
|
||||
fill_datas.append(submob.data[fill_names])
|
||||
submob.get_joint_products()
|
||||
has_fill = submob.has_fill()
|
||||
has_stroke = submob.has_stroke()
|
||||
if has_fill:
|
||||
data = submob.data[fill_names]
|
||||
data["base_point"][:] = data["point"][0]
|
||||
fill_datas.append(data)
|
||||
if self._use_winding_fill:
|
||||
# Add dummy
|
||||
fill_datas.append(submob.data[fill_names][-1:])
|
||||
fill_datas.append(data[-1:])
|
||||
else:
|
||||
fill_indices.append(submob.get_triangulation())
|
||||
if submob.has_stroke():
|
||||
submob.get_joint_products()
|
||||
if submob.stroke_behind:
|
||||
lst = back_stroke_data
|
||||
else:
|
||||
lst = stroke_datas
|
||||
# Add fill border
|
||||
if not has_stroke:
|
||||
names = list(stroke_names)
|
||||
names[names.index('stroke_rgba')] = 'fill_rgba'
|
||||
names[names.index('stroke_width')] = 'fill_border_width'
|
||||
border_stroke_data = submob.data[names]
|
||||
fill_border_datas.append(border_stroke_data)
|
||||
fill_border_datas.append(border_stroke_data[-1:])
|
||||
if has_stroke:
|
||||
lst = back_stroke_datas if submob.stroke_behind else stroke_datas
|
||||
lst.append(submob.data[stroke_names])
|
||||
# Set data array to be one longer than number of points,
|
||||
# with a dummy vertex added at the end. This is to ensure
|
||||
|
@ -1260,15 +1303,14 @@ class VMobject(Mobject):
|
|||
lst.append(submob.data[stroke_names][-1:])
|
||||
|
||||
shader_wrappers = [
|
||||
self.back_stroke_shader_wrapper.read_in(back_stroke_data),
|
||||
self.back_stroke_shader_wrapper.read_in(
|
||||
[*back_stroke_datas, *fill_border_datas]
|
||||
),
|
||||
self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None),
|
||||
self.stroke_shader_wrapper.read_in(stroke_datas),
|
||||
]
|
||||
|
||||
for sw in shader_wrappers:
|
||||
# Assume uniforms of the first family member
|
||||
sw.uniforms.update(family[0].get_uniforms())
|
||||
sw.depth_test = family[0].depth_test
|
||||
# TODO, account for submob uniforms separately?
|
||||
self.uniforms.update(family[0].uniforms)
|
||||
return [sw for sw in shader_wrappers if len(sw.vert_data) > 0]
|
||||
|
||||
|
||||
|
|
|
@ -288,13 +288,11 @@ class Scene(object):
|
|||
|
||||
def get_image(self) -> Image:
|
||||
if self.window is not None:
|
||||
self.window.size = self.camera.get_pixel_shape()
|
||||
self.window.swap_buffers()
|
||||
self.update_frame()
|
||||
self.window.swap_buffers()
|
||||
self.camera.use_window_fbo(False)
|
||||
self.camera.capture(*self.mobjects)
|
||||
image = self.camera.get_image()
|
||||
if self.window is not None:
|
||||
self.window.to_default_position()
|
||||
self.camera.use_window_fbo(True)
|
||||
return image
|
||||
|
||||
def show(self) -> None:
|
||||
|
@ -312,7 +310,6 @@ class Scene(object):
|
|||
|
||||
if self.window:
|
||||
self.window.clear()
|
||||
self.camera.clear()
|
||||
self.camera.capture(*self.mobjects)
|
||||
|
||||
if self.window:
|
||||
|
|
|
@ -7,20 +7,20 @@ import re
|
|||
import OpenGL.GL as gl
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from functools import lru_cache
|
||||
|
||||
from manimlib.utils.iterables import resize_array
|
||||
from manimlib.utils.shaders import get_shader_code_from_file
|
||||
from manimlib.utils.shaders import get_shader_program
|
||||
from manimlib.utils.shaders import image_path_to_texture
|
||||
from manimlib.utils.shaders import get_texture_id
|
||||
from manimlib.utils.shaders import get_fill_palette
|
||||
from manimlib.utils.shaders import get_fill_canvas
|
||||
from manimlib.utils.shaders import release_texture
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Dict
|
||||
from manimlib.typing import UniformDict
|
||||
|
||||
|
||||
# Mobjects that should be rendered with
|
||||
|
@ -37,7 +37,7 @@ class ShaderWrapper(object):
|
|||
vert_data: np.ndarray,
|
||||
vert_indices: Optional[np.ndarray] = None,
|
||||
shader_folder: Optional[str] = None,
|
||||
uniforms: Optional[dict[str, float | np.ndarray]] = None, # A dictionary mapping names of uniform variables
|
||||
uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables
|
||||
texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
|
||||
depth_test: bool = False,
|
||||
render_primitive: int = moderngl.TRIANGLE_STRIP,
|
||||
|
@ -47,20 +47,18 @@ class ShaderWrapper(object):
|
|||
self.vert_indices = (vert_indices or np.zeros(0)).astype(int)
|
||||
self.vert_attributes = vert_data.dtype.names
|
||||
self.shader_folder = shader_folder
|
||||
self.uniforms = dict(uniforms or {})
|
||||
self.uniforms: UniformDict = dict()
|
||||
self.depth_test = depth_test
|
||||
self.render_primitive = render_primitive
|
||||
|
||||
self.init_program_code()
|
||||
self.init_program()
|
||||
self.update_program_uniforms(uniforms or dict())
|
||||
if texture_paths is not None:
|
||||
self.init_textures(texture_paths)
|
||||
self.init_vao()
|
||||
self.refresh_id()
|
||||
|
||||
self.vbo = None
|
||||
self.ibo = None
|
||||
self.vao = None
|
||||
|
||||
def init_program_code(self) -> None:
|
||||
def get_code(name: str) -> str | None:
|
||||
return get_shader_code_from_file(
|
||||
|
@ -82,10 +80,16 @@ class ShaderWrapper(object):
|
|||
self.vert_format = moderngl.detect_format(self.program, self.vert_attributes)
|
||||
|
||||
def init_textures(self, texture_paths: dict[str, str]):
|
||||
for name, path in texture_paths.items():
|
||||
texture = image_path_to_texture(path, self.ctx)
|
||||
tid = get_texture_id(texture)
|
||||
self.uniforms[name] = tid
|
||||
names_to_ids = {
|
||||
name: get_texture_id(image_path_to_texture(path, self.ctx))
|
||||
for name, path in texture_paths.items()
|
||||
}
|
||||
self.update_program_uniforms(names_to_ids)
|
||||
|
||||
def init_vao(self):
|
||||
self.vbo = None
|
||||
self.ibo = None
|
||||
self.vao = None
|
||||
|
||||
def __eq__(self, shader_wrapper: ShaderWrapper):
|
||||
return all((
|
||||
|
@ -93,7 +97,7 @@ class ShaderWrapper(object):
|
|||
np.all(self.vert_indices == shader_wrapper.vert_indices),
|
||||
self.shader_folder == shader_wrapper.shader_folder,
|
||||
all(
|
||||
np.all(self.uniforms[key] == shader_wrapper.uniforms[key])
|
||||
self.uniforms[key] == shader_wrapper.uniforms[key]
|
||||
for key in self.uniforms
|
||||
),
|
||||
self.depth_test == shader_wrapper.depth_test,
|
||||
|
@ -105,11 +109,7 @@ class ShaderWrapper(object):
|
|||
result.ctx = self.ctx
|
||||
result.vert_data = self.vert_data.copy()
|
||||
result.vert_indices = self.vert_indices.copy()
|
||||
if self.uniforms:
|
||||
result.uniforms = {key: np.array(value) for key, value in self.uniforms.items()}
|
||||
result.vao = None
|
||||
result.vbo = None
|
||||
result.ibo = None
|
||||
result.init_vao()
|
||||
return result
|
||||
|
||||
def is_valid(self) -> bool:
|
||||
|
@ -217,20 +217,23 @@ class ShaderWrapper(object):
|
|||
def pre_render(self):
|
||||
self.set_ctx_depth_test(self.depth_test)
|
||||
self.set_ctx_clip_plane(self.use_clip_plane())
|
||||
self.update_program_uniforms()
|
||||
|
||||
def render(self):
|
||||
assert(self.vao is not None)
|
||||
self.vao.render()
|
||||
|
||||
def update_program_uniforms(self):
|
||||
def update_program_uniforms(self, uniforms: UniformDict, universal: bool = False):
|
||||
if self.program is None:
|
||||
return
|
||||
for name, value in self.uniforms.items():
|
||||
if name in self.program:
|
||||
if isinstance(value, np.ndarray) and value.ndim > 0:
|
||||
value = tuple(value)
|
||||
self.program[name].value = value
|
||||
for name, value in uniforms.items():
|
||||
if name not in self.program:
|
||||
continue
|
||||
if isinstance(value, np.ndarray) and value.ndim > 0:
|
||||
value = tuple(value)
|
||||
if universal and self.uniforms.get(name, None) == value:
|
||||
continue
|
||||
self.program[name].value = value
|
||||
self.uniforms[name] = value
|
||||
|
||||
def get_vertex_buffer_object(self, refresh: bool = True):
|
||||
if refresh:
|
||||
|
@ -245,8 +248,9 @@ class ShaderWrapper(object):
|
|||
def generate_vao(self, refresh: bool = True):
|
||||
self.release()
|
||||
# Data buffer
|
||||
vbo = self.get_vertex_buffer_object(refresh)
|
||||
ibo = self.get_index_buffer_object(refresh)
|
||||
vbo = self.vbo = self.get_vertex_buffer_object(refresh)
|
||||
ibo = self.ibo = self.get_index_buffer_object(refresh)
|
||||
|
||||
# Vertex array object
|
||||
self.vao = self.ctx.vertex_array(
|
||||
program=self.program,
|
||||
|
@ -273,7 +277,7 @@ class FillShaderWrapper(ShaderWrapper):
|
|||
**kwargs
|
||||
):
|
||||
super().__init__(ctx, *args, **kwargs)
|
||||
|
||||
self.fill_canvas = get_fill_canvas(self.ctx)
|
||||
|
||||
def render(self):
|
||||
vao = self.vao
|
||||
|
@ -285,13 +289,26 @@ class FillShaderWrapper(ShaderWrapper):
|
|||
return
|
||||
|
||||
original_fbo = self.ctx.fbo
|
||||
texture_fbo, texture_vao = get_fill_palette(self.ctx)
|
||||
texture_fbo, texture_vao, null_rgb = self.fill_canvas
|
||||
|
||||
texture_fbo.clear()
|
||||
texture_fbo.clear(*null_rgb, 0.0)
|
||||
texture_fbo.use()
|
||||
vao.render()
|
||||
gl.glBlendFuncSeparate(
|
||||
# Ordinary blending for colors
|
||||
gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA,
|
||||
# Just take the max of the alphas, given the shenanigans
|
||||
# with how alphas are being used to compute winding numbers
|
||||
gl.GL_ONE, gl.GL_ONE,
|
||||
)
|
||||
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX)
|
||||
self.ctx.blend_equation = moderngl.FUNC_ADD, moderngl.MAX
|
||||
|
||||
vao.render(moderngl.TRIANGLE_STRIP)
|
||||
|
||||
original_fbo.use()
|
||||
self.ctx.blend_func = (moderngl.ONE, moderngl.ONE_MINUS_SRC_ALPHA)
|
||||
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
||||
|
||||
texture_vao.render(moderngl.TRIANGLE_STRIP)
|
||||
self.ctx.blend_func = (moderngl.DEFAULT_BLENDING)
|
||||
|
||||
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
|
|
|
@ -7,6 +7,7 @@ in float fill_all;
|
|||
in float orientation;
|
||||
in vec2 uv_coords;
|
||||
in vec3 point;
|
||||
in vec3 unit_normal;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
|
@ -14,7 +15,7 @@ out vec4 frag_color;
|
|||
|
||||
void main() {
|
||||
if (color.a == 0) discard;
|
||||
frag_color = finalize_color(color, point, vec3(0.0, 0.0, 1.0));
|
||||
frag_color = finalize_color(color, point, unit_normal);
|
||||
/*
|
||||
We want negatively oriented triangles to be canceled with positively
|
||||
oriented ones. The easiest way to do this is to give them negative alpha,
|
||||
|
@ -30,9 +31,9 @@ void main() {
|
|||
is changed to -alpha / (1 - alpha). This has a singularity at alpha = 1,
|
||||
so we cap it at a value very close to 1. Effectively, the purpose of this
|
||||
cap is to make sure the original fragment color can be recovered even after
|
||||
blending with an alpha = 1 color.
|
||||
blending with an (alpha = 1) color.
|
||||
*/
|
||||
float a = 0.999 * frag_color.a;
|
||||
float a = 0.99 * frag_color.a;
|
||||
if(winding && orientation < 0) a = -a / (1 - a);
|
||||
frag_color.a = a;
|
||||
|
||||
|
|
|
@ -9,11 +9,13 @@ in vec3 verts[3];
|
|||
in vec4 v_color[3];
|
||||
in vec3 v_base_point[3];
|
||||
in float v_vert_index[3];
|
||||
in vec3 v_unit_normal[3];
|
||||
|
||||
out vec4 color;
|
||||
out float fill_all;
|
||||
out float orientation;
|
||||
out vec3 point;
|
||||
out vec3 unit_normal;
|
||||
// uv space is where the curve coincides with y = x^2
|
||||
out vec2 uv_coords;
|
||||
|
||||
|
@ -26,19 +28,19 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3](
|
|||
|
||||
// Analog of import for manim only
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT get_unit_normal.glsl
|
||||
|
||||
|
||||
void emit_triangle(vec3 points[3], vec4 v_color[3]){
|
||||
vec3 unit_normal = get_unit_normal(points[0], points[1], points[2]);
|
||||
orientation = winding ? sign(unit_normal.z) : 1.0;
|
||||
orientation = sign(determinant(mat3(
|
||||
unit_normal,
|
||||
points[1] - points[0],
|
||||
points[2] - points[0]
|
||||
)));
|
||||
|
||||
for(int i = 0; i < 3; i++){
|
||||
uv_coords = SIMPLE_QUADRATIC[i];
|
||||
color = v_color[i];
|
||||
point = points[i];
|
||||
// Pure black will be used to discard fragments later
|
||||
if(winding && color.rgb == vec3(0.0)) color.rgb += vec3(0.01);
|
||||
gl_Position = get_gl_Position(points[i]);
|
||||
EmitVertex();
|
||||
}
|
||||
|
@ -63,7 +65,8 @@ void main(){
|
|||
// the first anchor is set equal to that anchor
|
||||
if (verts[0] == verts[1]) return;
|
||||
|
||||
vec3 mid_vert;
|
||||
unit_normal = v_unit_normal[1];
|
||||
|
||||
if(winding){
|
||||
// Emit main triangle
|
||||
fill_all = 1.0;
|
||||
|
|
|
@ -3,15 +3,18 @@
|
|||
in vec3 point;
|
||||
in vec4 fill_rgba;
|
||||
in vec3 base_point;
|
||||
in vec3 unit_normal;
|
||||
|
||||
out vec3 verts; // Bezier control point
|
||||
out vec4 v_color;
|
||||
out vec3 v_base_point;
|
||||
out vec3 v_unit_normal;
|
||||
out float v_vert_index;
|
||||
|
||||
void main(){
|
||||
verts = point;
|
||||
v_color = fill_rgba;
|
||||
v_base_point = base_point;
|
||||
v_unit_normal = unit_normal;
|
||||
v_vert_index = gl_VertexID;
|
||||
}
|
|
@ -49,6 +49,12 @@ vec3 get_joint_unit_normal(vec4 joint_product){
|
|||
}
|
||||
|
||||
|
||||
vec4 normalized_joint_product(vec4 joint_product){
|
||||
float norm = length(joint_product);
|
||||
return (norm > 1e-10) ? joint_product / norm : vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
|
||||
void create_joint(
|
||||
vec4 joint_product,
|
||||
vec3 unit_tan,
|
||||
|
@ -78,6 +84,25 @@ void create_joint(
|
|||
changing_c1 = static_c1 + shift * unit_tan;
|
||||
}
|
||||
|
||||
vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw){
|
||||
/*
|
||||
Perpendicular vectors to the left of the curve
|
||||
*/
|
||||
float buff = 0.5 * v_stroke_width[index] + aaw;
|
||||
// Add correction for sharp angles to prevent weird bevel effects
|
||||
if(joint_product.w < -0.9) buff *= 10 * (joint_product.w + 1.0);
|
||||
vec3 normal = get_joint_unit_normal(joint_product);
|
||||
// Set global unit normal
|
||||
unit_normal = normal;
|
||||
// Choose the "outward" normal direction
|
||||
if(normal.z < 0) normal *= -1;
|
||||
if(bool(flat_stroke)){
|
||||
return buff * normalize(cross(normal, tangent));
|
||||
}else{
|
||||
return buff * normalize(cross(camera_position - point, tangent));
|
||||
}
|
||||
}
|
||||
|
||||
// This function is responsible for finding the corners of
|
||||
// a bounding region around the bezier curve, which can be
|
||||
// emitted as a triangle fan, with vertices vaguely close
|
||||
|
@ -95,40 +120,15 @@ void get_corners(
|
|||
float aaw,
|
||||
out vec3 corners[6]
|
||||
){
|
||||
|
||||
float buff0 = 0.5 * v_stroke_width[0] + aaw;
|
||||
float buff2 = 0.5 * v_stroke_width[2] + aaw;
|
||||
|
||||
vec4 jp0 = normalize(v_joint_product[0]);
|
||||
vec4 jp2 = normalize(v_joint_product[2]);
|
||||
|
||||
// Add correction for sharp angles to prevent weird bevel effects
|
||||
if(jp0.w < -0.9) buff0 *= 10 * (jp0.w + 1.0);
|
||||
if(jp2.w < -0.9) buff2 *= 10 * (jp2.w + 1.0);
|
||||
|
||||
// Unit normal and joint angles
|
||||
vec3 normal0 = get_joint_unit_normal(jp0);
|
||||
vec3 normal2 = get_joint_unit_normal(jp2);
|
||||
// Set global unit normal
|
||||
unit_normal = normal0;
|
||||
|
||||
// Choose the "outward" normal direction
|
||||
normal0 *= sign(normal0.z);
|
||||
normal2 *= sign(normal2.z);
|
||||
|
||||
vec3 p0_perp;
|
||||
vec3 p2_perp;
|
||||
if(bool(flat_stroke)){
|
||||
// Perpendicular vectors to the left of the curve
|
||||
p0_perp = buff0 * normalize(cross(normal0, v01));
|
||||
p2_perp = buff2 * normalize(cross(normal2, v12));
|
||||
}else{
|
||||
// p0_perp = buff0 * normal0;
|
||||
// p2_perp = buff2 * normal2;
|
||||
p0_perp = buff0 * normalize(cross(camera_position - p0, v01));
|
||||
p2_perp = buff2 * normalize(cross(camera_position - p2, v12));
|
||||
}
|
||||
bool linear = bool(is_linear);
|
||||
vec4 jp0 = normalized_joint_product(v_joint_product[0]);
|
||||
vec4 jp2 = normalized_joint_product(v_joint_product[2]);
|
||||
vec3 p0_perp = get_perp(0, jp0, p0, v01, aaw);
|
||||
vec3 p2_perp = get_perp(2, jp2, p2, v12, aaw);
|
||||
vec3 p1_perp = 0.5 * (p0_perp + p2_perp);
|
||||
if(linear){
|
||||
p1_perp *= (0.5 * v_stroke_width[1] + aaw) / length(p1_perp);
|
||||
}
|
||||
|
||||
// The order of corners should be for a triangle_strip.
|
||||
vec3 c0 = p0 + p0_perp;
|
||||
|
@ -139,14 +139,15 @@ void get_corners(
|
|||
vec3 c5 = p2 - p2_perp;
|
||||
// Move the inner middle control point to make
|
||||
// room for the curve
|
||||
float orientation = dot(normal0, v_joint_product[1].xyz);
|
||||
if(orientation >= 0.0) c2 = 0.5 * (c0 + c4);
|
||||
else if(orientation < 0.0) c3 = 0.5 * (c1 + c5);
|
||||
// float orientation = dot(unit_normal, v_joint_product[1].xyz);
|
||||
float orientation = v_joint_product[1].z;
|
||||
if(!linear && orientation >= 0.0) c2 = 0.5 * (c0 + c4);
|
||||
else if(!linear && orientation < 0.0) c3 = 0.5 * (c1 + c5);
|
||||
|
||||
// Account for previous and next control points
|
||||
if(bool(flat_stroke)){
|
||||
create_joint(jp0, v01, buff0, c1, c1, c0, c0);
|
||||
create_joint(jp2, -v12, buff2, c5, c5, c4, c4);
|
||||
create_joint(jp0, v01, length(p0_perp), c1, c1, c0, c0);
|
||||
create_joint(jp2, -v12, length(p2_perp), c5, c5, c4, c4);
|
||||
}
|
||||
|
||||
corners = vec3[6](c0, c1, c2, c3, c4, c5);
|
||||
|
@ -167,8 +168,9 @@ void main() {
|
|||
vec3 v01 = normalize(p1 - p0);
|
||||
vec3 v12 = normalize(p2 - p1);
|
||||
|
||||
float cos_angle = normalize(v_joint_product[1]).w;
|
||||
is_linear = float(cos_angle > COS_THRESHOLD);
|
||||
|
||||
vec4 jp1 = normalized_joint_product(v_joint_product[1]);
|
||||
is_linear = float(jp1.w > COS_THRESHOLD);
|
||||
|
||||
// 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
|
||||
|
@ -195,7 +197,7 @@ void main() {
|
|||
float sign = vec2(-1, 1)[i % 2];
|
||||
// In this case, we only really care about
|
||||
// the v coordinate
|
||||
uv_coords = vec2(0, sign * (0.5 * max_sw + scaled_aaw));
|
||||
uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw));
|
||||
uv_anti_alias_width = scaled_aaw;
|
||||
uv_stroke_width = stroke_width;
|
||||
}else{
|
||||
|
|
|
@ -19,6 +19,8 @@ if TYPE_CHECKING:
|
|||
]
|
||||
Selector = Union[SingleSelector, Iterable[SingleSelector]]
|
||||
|
||||
UniformDict = Dict[str, float | bool | np.ndarray | tuple]
|
||||
|
||||
# These are various alternate names for np.ndarray meant to specify
|
||||
# certain shapes.
|
||||
#
|
||||
|
|
|
@ -133,7 +133,7 @@ def arrays_match(arr1: np.ndarray, arr2: np.ndarray) -> bool:
|
|||
|
||||
|
||||
def array_is_constant(arr: np.ndarray) -> bool:
|
||||
return len(arr) > 0 and not (arr - arr[0]).any()
|
||||
return len(arr) > 0 and (arr == arr[0]).all()
|
||||
|
||||
|
||||
def hash_obj(obj: object) -> int:
|
||||
|
|
|
@ -9,6 +9,7 @@ import numpy as np
|
|||
|
||||
from manimlib.constants import DEFAULT_PIXEL_HEIGHT
|
||||
from manimlib.constants import DEFAULT_PIXEL_WIDTH
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.directories import get_shader_dir
|
||||
from manimlib.utils.file_ops import find_file
|
||||
|
||||
|
@ -102,18 +103,37 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
|
|||
|
||||
|
||||
@lru_cache()
|
||||
def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]:
|
||||
def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
|
||||
"""
|
||||
Creates a texture, loaded into a frame buffer, and a vao
|
||||
which can display that texture as a simple quad onto a screen.
|
||||
Because VMobjects with fill are rendered in a funny way, using
|
||||
alpha blending to effectively compute the winding number around
|
||||
each pixel, they need to be rendered to a separate texture, which
|
||||
is then composited onto the ordinary frame buffer.
|
||||
|
||||
This returns a texture, loaded into a frame buffer, and a vao
|
||||
which can display that texture as a simple quad onto a screen,
|
||||
along with the rgb value which is meant to be discarded.
|
||||
"""
|
||||
size = (2 * DEFAULT_PIXEL_WIDTH, 2 * DEFAULT_PIXEL_HEIGHT)
|
||||
cam_config = get_customization()['camera_resolutions']
|
||||
res_name = cam_config['default_resolution']
|
||||
size = tuple(map(int, cam_config[res_name].split("x")))
|
||||
|
||||
# Important to make sure dtype is floating point (not fixed point)
|
||||
# so that alpha values can be negative and are not clipped
|
||||
texture = ctx.texture(size=size, components=4, dtype='f4')
|
||||
texture = ctx.texture(size=size, components=4, dtype='f2')
|
||||
depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used
|
||||
texture_fbo = ctx.framebuffer(texture, depth_buffer)
|
||||
|
||||
# We'll paint onto a canvas with initially negative rgbs, and
|
||||
# discard any pixels remaining close to this value. This is
|
||||
# because alphas are effectively being used for another purpose,
|
||||
# and we don't want to overlap with any colors one might actually
|
||||
# use. It should be negative enough to be distinguishable from
|
||||
# ordinary colors with some margin, but the farther it's pulled back
|
||||
# from zero the more it will be true that overlapping filled objects
|
||||
# with transparency have an unnaturally bright composition.
|
||||
null_rgb = (-0.25, -0.25, -0.25)
|
||||
|
||||
simple_program = ctx.program(
|
||||
vertex_shader='''
|
||||
#version 330
|
||||
|
@ -130,33 +150,27 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]:
|
|||
#version 330
|
||||
|
||||
uniform sampler2D Texture;
|
||||
uniform float v_nudge;
|
||||
uniform float h_nudge;
|
||||
uniform vec3 null_rgb;
|
||||
|
||||
in vec2 v_textcoord;
|
||||
out vec4 frag_color;
|
||||
out vec4 color;
|
||||
|
||||
const float MIN_DIST_TO_NULL = 0.2;
|
||||
|
||||
void main() {
|
||||
// Apply poor man's anti-aliasing
|
||||
vec2 tc0 = v_textcoord + vec2(0, 0);
|
||||
vec2 tc1 = v_textcoord + vec2(0, h_nudge);
|
||||
vec2 tc2 = v_textcoord + vec2(v_nudge, 0);
|
||||
vec2 tc3 = v_textcoord + vec2(v_nudge, h_nudge);
|
||||
frag_color =
|
||||
0.25 * texture(Texture, tc0) +
|
||||
0.25 * texture(Texture, tc1) +
|
||||
0.25 * texture(Texture, tc2) +
|
||||
0.25 * texture(Texture, tc3);
|
||||
if(distance(frag_color.rgb, vec3(0.0)) < 1e-3) discard;
|
||||
color = texture(Texture, v_textcoord);
|
||||
if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard;
|
||||
|
||||
// Un-blend from the null value
|
||||
color.rgb -= (1 - color.a) * null_rgb;
|
||||
|
||||
//TODO, set gl_FragDepth;
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
simple_program['Texture'].value = get_texture_id(texture)
|
||||
# Half pixel width/height
|
||||
simple_program['h_nudge'].value = 0.5 / size[0]
|
||||
simple_program['v_nudge'].value = 0.5 / size[1]
|
||||
simple_program['null_rgb'].value = null_rgb
|
||||
|
||||
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
|
||||
fill_texture_vao = ctx.simple_vertex_array(
|
||||
|
@ -164,4 +178,4 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]:
|
|||
ctx.buffer(verts.astype('f4').tobytes()),
|
||||
'texcoord',
|
||||
)
|
||||
return (texture_fbo, fill_texture_vao)
|
||||
return (texture_fbo, fill_texture_vao, null_rgb)
|
||||
|
|
Loading…
Add table
Reference in a new issue