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

Miscellaneous bug fixes
This commit is contained in:
Grant Sanderson 2023-01-27 19:28:40 -08:00 committed by GitHub
commit f296dd8df5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 324 additions and 219 deletions

View file

@ -73,6 +73,7 @@ from manimlib.utils.iterables import *
from manimlib.utils.paths import * from manimlib.utils.paths import *
from manimlib.utils.rate_functions import * from manimlib.utils.rate_functions import *
from manimlib.utils.simple_functions import * from manimlib.utils.simple_functions import *
from manimlib.utils.shaders import *
from manimlib.utils.sounds import * from manimlib.utils.sounds import *
from manimlib.utils.space_ops import * from manimlib.utils.space_ops import *
from manimlib.utils.tex import * from manimlib.utils.tex import *

View file

@ -6,6 +6,8 @@ from manimlib.animation.animation import Animation
from manimlib.animation.animation import prepare_animation from manimlib.animation.animation import prepare_animation
from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import _AnimationBuilder
from manimlib.mobject.mobject import Group 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 integer_interpolate
from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import interpolate
from manimlib.utils.iterables import remove_list_redundancies from manimlib.utils.iterables import remove_list_redundancies
@ -14,7 +16,7 @@ from manimlib.utils.simple_functions import clip
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable from typing import Callable, Optional
from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Mobject
from manimlib.scene.scene import Scene from manimlib.scene.scene import Scene
@ -28,8 +30,8 @@ class AnimationGroup(Animation):
*animations: Animation | _AnimationBuilder, *animations: Animation | _AnimationBuilder,
run_time: float = -1, # If negative, default to sum of inputed animation runtimes run_time: float = -1, # If negative, default to sum of inputed animation runtimes
lag_ratio: float = 0.0, lag_ratio: float = 0.0,
group: Mobject | None = None, group: Optional[Mobject] = None,
group_type: type = Group, group_type: Optional[type] = None,
**kwargs **kwargs
): ):
self.animations = [prepare_animation(anim) for anim in animations] 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.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.run_time = self.max_end_time if run_time < 0 else run_time
self.lag_ratio = lag_ratio self.lag_ratio = lag_ratio
self.group = group mobs = remove_list_redundancies([a.mobject for a in self.animations])
if self.group is None: if group is not None:
self.group = group_type(*remove_list_redundancies( self.group = group
[anim.mobject for anim in self.animations] 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__( super().__init__(
self.group, self.group,

View file

@ -100,7 +100,8 @@ class DrawBorderThenFill(Animation):
def begin(self) -> None: def begin(self) -> None:
# Trigger triangulation calculation # Trigger triangulation calculation
for submob in self.mobject.get_family(): for submob in self.mobject.get_family():
submob.get_triangulation() if not submob._use_winding_fill:
submob.get_triangulation()
self.outline = self.get_outline() self.outline = self.get_outline()
super().begin() super().begin()

View file

@ -265,7 +265,7 @@ class FlashAround(VShowPassingFlash):
**kwargs **kwargs
): ):
path = self.get_path(mobject, buff) path = self.get_path(mobject, buff)
if mobject.is_fixed_in_frame: if mobject.is_fixed_in_frame():
path.fix_in_frame() path.fix_in_frame()
path.insert_n_curves(n_inserted_curves) path.insert_n_curves(n_inserted_curves)
path.set_points(path.get_points_without_null_curves()) path.set_points(path.get_points_without_null_curves())

View file

@ -32,7 +32,6 @@ class TransformMatchingParts(AnimationGroup):
mismatch_animation: type = Transform, mismatch_animation: type = Transform,
run_time: float = 2, run_time: float = 2,
lag_ratio: float = 0, lag_ratio: float = 0,
group_type: type = Group,
**kwargs, **kwargs,
): ):
self.source = source self.source = source
@ -76,7 +75,6 @@ class TransformMatchingParts(AnimationGroup):
*self.anims, *self.anims,
run_time=run_time, run_time=run_time,
lag_ratio=lag_ratio, lag_ratio=lag_ratio,
group_type=group_type,
) )
def add_transform( def add_transform(
@ -151,7 +149,6 @@ class TransformMatchingStrings(TransformMatchingParts):
super().__init__( super().__init__(
source, target, source, target,
matched_pairs=matched_pairs, matched_pairs=matched_pairs,
group_type=VGroup,
**kwargs, **kwargs,
) )

View file

@ -17,6 +17,7 @@ from manimlib.utils.color import color_to_rgba
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Optional
from manimlib.typing import ManimColor, Vect3 from manimlib.typing import ManimColor, Vect3
from manimlib.window import Window from manimlib.window import Window
@ -24,8 +25,8 @@ if TYPE_CHECKING:
class Camera(object): class Camera(object):
def __init__( def __init__(
self, self,
window: Window | None = None, window: Optional[Window] = None,
background_image: str | None = None, background_image: Optional[str] = None,
frame_config: dict = dict(), frame_config: dict = dict(),
pixel_width: int = DEFAULT_PIXEL_WIDTH, pixel_width: int = DEFAULT_PIXEL_WIDTH,
pixel_height: int = DEFAULT_PIXEL_HEIGHT, pixel_height: int = DEFAULT_PIXEL_HEIGHT,
@ -62,31 +63,38 @@ class Camera(object):
)) ))
self.uniforms = dict() self.uniforms = dict()
self.init_frame(**frame_config) self.init_frame(**frame_config)
self.init_context(window) self.init_context()
self.init_fbo()
self.init_light_source() self.init_light_source()
def init_frame(self, **config) -> None: def init_frame(self, **config) -> None:
self.frame = CameraFrame(**config) self.frame = CameraFrame(**config)
def init_context(self, window: Window | None = None) -> None: def init_context(self) -> None:
self.window = window if self.window is None:
if window is None:
self.ctx = moderngl.create_standalone_context() self.ctx = moderngl.create_standalone_context()
self.fbo = self.get_fbo(self.samples)
else: else:
self.ctx = window.ctx self.ctx = self.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.enable(moderngl.PROGRAM_POINT_SIZE) self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND) 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 # This is the frame buffer we'll draw into when emitting frames
self.draw_fbo = self.get_fbo(samples=0) 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: def init_light_source(self) -> None:
self.light_source = Point(self.light_source_position) self.light_source = Point(self.light_source_position)
@ -210,6 +218,7 @@ class Camera(object):
# Rendering # Rendering
def capture(self, *mobjects: Mobject) -> None: def capture(self, *mobjects: Mobject) -> None:
self.clear()
self.refresh_uniforms() self.refresh_uniforms()
self.fbo.use() self.fbo.use()
for mobject in mobjects: for mobject in mobjects:

View file

@ -685,7 +685,6 @@ class Arrow(Line):
self.width_to_tip_len = width_to_tip_len self.width_to_tip_len = width_to_tip_len
self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio 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_width_to_length_ratio = max_width_to_length_ratio
self.max_stroke_width = stroke_width
super().__init__( super().__init__(
start, end, start, end,
stroke_color=stroke_color, stroke_color=stroke_color,
@ -715,22 +714,21 @@ class Arrow(Line):
else: else:
alpha = tip_len / arc_len alpha = tip_len / arc_len
self.pointwise_become_partial(self, 0, 1 - alpha) 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) self.add_line_to(prev_end)
return self return self
@Mobject.affects_data
def create_tip_with_stroke_width(self): def create_tip_with_stroke_width(self):
if not self.has_points(): if self.get_num_points() < 3:
return self return self
width = min( tip_width = self.tip_width_ratio * min(
self.max_stroke_width, float(self.get_stroke_width()),
self.max_width_to_length_ratio * self.get_length(), self.max_width_to_length_ratio * self.get_length(),
) )
widths_array = np.full(self.get_num_points(), width) self.data['stroke_width'][:-3] = self.data['stroke_width'][0]
if len(widths_array) > 3: self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 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 return self
def reset_tip(self): def reset_tip(self):
@ -747,13 +745,14 @@ class Arrow(Line):
*args, **kwargs *args, **kwargs
): ):
super().set_stroke(color=color, width=width, *args, **kwargs) super().set_stroke(color=color, width=width, *args, **kwargs)
if isinstance(width, numbers.Number): if self.has_points():
self.max_stroke_width = width self.reset_tip()
self.create_tip_with_stroke_width()
return self return self
def _handle_scale_side_effects(self, scale_factor: float): 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): class FillArrow(Line):

View file

@ -50,7 +50,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Iterable, Union, Tuple from typing import Callable, Iterable, Union, Tuple
import numpy.typing as npt 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 from moderngl.context import Context
TimeBasedUpdater = Callable[["Mobject", float], "Mobject" | None] TimeBasedUpdater = Callable[["Mobject", float], "Mobject" | None]
@ -88,7 +88,7 @@ class Mobject(object):
self.opacity = opacity self.opacity = opacity
self.shading = shading self.shading = shading
self.texture_paths = texture_paths 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 self.depth_test = depth_test
# Internal state # Internal state
@ -131,8 +131,8 @@ class Mobject(object):
self.data = np.zeros(length, dtype=self.shader_dtype) self.data = np.zeros(length, dtype=self.shader_dtype)
def init_uniforms(self): def init_uniforms(self):
self.uniforms: dict[str, float | np.ndarray] = { self.uniforms: UniformDict = {
"is_fixed_in_frame": float(self.is_fixed_in_frame), "is_fixed_in_frame": float(self._is_fixed_in_frame),
"shading": np.array(self.shading, dtype=float), "shading": np.array(self.shading, dtype=float),
} }
@ -408,14 +408,15 @@ class Mobject(object):
self.assemble_family() self.assemble_family()
return self return self
def remove(self, *mobjects: Mobject, reassemble: bool = True): def remove(self, *to_remove: Mobject, reassemble: bool = True):
for mobject in mobjects: for parent in self.get_family():
if mobject in self.submobjects: for child in to_remove:
self.submobjects.remove(mobject) if child in parent.submobjects:
if self in mobject.parents: parent.submobjects.remove(child)
mobject.parents.remove(self) if parent in child.parents:
if reassemble: child.parents.remove(parent)
self.assemble_family() if reassemble:
parent.assemble_family()
return self return self
def add_to_back(self, *mobjects: Mobject): def add_to_back(self, *mobjects: Mobject):
@ -591,19 +592,22 @@ class Mobject(object):
result._shaders_initialized = False result._shaders_initialized = False
result._data_has_changed = True result._data_has_changed = True
@stash_mobject_pointers
def copy(self, deep: bool = False): def copy(self, deep: bool = False):
if deep: if deep:
return self.deepcopy() return self.deepcopy()
result = copy.copy(self) result = copy.copy(self)
# The line above is only a shallow copy, so the internal result.parents = []
# data which are numpyu arrays or other mobjects still 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. # need to be further copied.
result.data = self.data.copy() result.data = self.data.copy()
result.uniforms = { result.uniforms = {
key: np.array(value) key: value.copy() if isinstance(value, np.ndarray) else value
for key, value in self.uniforms.items() for key, value in self.uniforms.items()
} }
@ -622,7 +626,7 @@ class Mobject(object):
result._data_has_changed = True result._data_has_changed = True
family = self.get_family() 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 isinstance(value, Mobject) and value is not self:
if value in family: if value in family:
setattr(result, attr, result.family[self.family.index(value)]) setattr(result, attr, result.family[self.family.index(value)])
@ -1765,20 +1769,26 @@ class Mobject(object):
self.locked_data_keys = set(keys) self.locked_data_keys = set(keys)
def lock_matching_data(self, mobject1: Mobject, mobject2: Mobject): def lock_matching_data(self, mobject1: Mobject, mobject2: Mobject):
for sm, sm1, sm2 in zip(self.get_family(), mobject1.get_family(), mobject2.get_family()): tuples = zip(
if sm.data.dtype == sm1.data.dtype == sm2.data.dtype: self.get_family(),
names = sm.data.dtype.names mobject1.get_family(),
sm.lock_data(filter( mobject2.get_family(),
lambda name: arrays_match(sm1.data[name], sm2.data[name]), )
names, for sm, sm1, sm2 in tuples:
)) if not sm.data.dtype == sm1.data.dtype == sm2.data.dtype:
sm.const_data_keys = set(filter( continue
lambda name: all( names = sm.data.dtype.names
array_is_constant(mob.data[name]) sm.lock_data(filter(
for mob in (sm, sm1, sm2) lambda name: arrays_match(sm1.data[name], sm2.data[name]),
), names,
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 return self
@ -1799,17 +1809,19 @@ class Mobject(object):
return wrapper return wrapper
@affects_shader_info_id @affects_shader_info_id
def fix_in_frame(self): def fix_in_frame(self, recurse: bool = True):
self.uniforms["is_fixed_in_frame"] = 1.0 for mob in self.get_family(recurse):
self.is_fixed_in_frame = True mob.uniforms["is_fixed_in_frame"] = 1.0
return self return self
@affects_shader_info_id @affects_shader_info_id
def unfix_from_frame(self): def unfix_from_frame(self):
self.uniforms["is_fixed_in_frame"] = 0.0 self.uniforms["is_fixed_in_frame"] = 0.0
self.is_fixed_in_frame = False
return self return self
def is_fixed_in_frame(self) -> bool:
return bool(self.uniforms["is_fixed_in_frame"])
@affects_shader_info_id @affects_shader_info_id
def apply_depth_test(self): def apply_depth_test(self):
self.depth_test = True self.depth_test = True
@ -1894,7 +1906,7 @@ class Mobject(object):
self.shader_wrapper.vert_data = self.get_shader_data() self.shader_wrapper.vert_data = self.get_shader_data()
self.shader_wrapper.vert_indices = self.get_shader_vert_indices() 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 self.shader_wrapper.depth_test = self.depth_test
return self.shader_wrapper return self.shader_wrapper
@ -1931,8 +1943,9 @@ class Mobject(object):
shader_wrapper.generate_vao() shader_wrapper.generate_vao()
self._data_has_changed = False self._data_has_changed = False
for shader_wrapper in self.shader_wrappers: for shader_wrapper in self.shader_wrappers:
shader_wrapper.uniforms.update(self.get_uniforms()) shader_wrapper.depth_test = self.depth_test
shader_wrapper.uniforms.update(camera_uniforms) shader_wrapper.update_program_uniforms(self.get_uniforms())
shader_wrapper.update_program_uniforms(camera_uniforms, universal=True)
shader_wrapper.pre_render() shader_wrapper.pre_render()
shader_wrapper.render() shader_wrapper.render()
@ -2048,7 +2061,7 @@ class Group(Mobject):
raise Exception("All submobjects must be of type Mobject") raise Exception("All submobjects must be of type Mobject")
Mobject.__init__(self, **kwargs) Mobject.__init__(self, **kwargs)
self.add(*mobjects) 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() self.fix_in_frame()
def __add__(self, other: Mobject | Group): def __add__(self, other: Mobject | Group):

View file

@ -74,6 +74,7 @@ class SVGMobject(VMobject):
super().__init__(**kwargs ) super().__init__(**kwargs )
self.init_svg_mobject() self.init_svg_mobject()
self.ensure_positive_orientation()
# Rather than passing style into super().__init__ # Rather than passing style into super().__init__
# do it after svg has been taken in # do it after svg has been taken in
@ -320,10 +321,6 @@ class VMobjectFromSVGPath(VMobject):
self.set_points(self.get_points_without_null_curves()) self.set_points(self.get_points_without_null_curves())
# So triangulation doesn't get messed up # So triangulation doesn't get messed up
self.subdivide_intersections() 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 # Save for future use
PATH_TO_POINTS[path_string] = self.get_points().copy() PATH_TO_POINTS[path_string] = self.get_points().copy()
else: else:

View file

@ -195,6 +195,7 @@ class Surface(Mobject):
).reshape(shape) ).reshape(shape)
return points.reshape((nu * nv, *resolution[2:])) return points.reshape((nu * nv, *resolution[2:]))
@Mobject.affects_data
def sort_faces_back_to_front(self, vect: Vect3 = OUT): def sort_faces_back_to_front(self, vect: Vect3 = OUT):
tri_is = self.triangle_indices tri_is = self.triangle_indices
points = self.get_points() points = self.get_points()

View file

@ -62,8 +62,10 @@ class VMobject(Mobject):
('joint_product', np.float32, (4,)), ('joint_product', np.float32, (4,)),
('fill_rgba', np.float32, (4,)), ('fill_rgba', np.float32, (4,)),
('base_point', np.float32, (3,)), ('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'] stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product']
fill_render_primitive: int = moderngl.TRIANGLE_STRIP fill_render_primitive: int = moderngl.TRIANGLE_STRIP
@ -91,6 +93,7 @@ class VMobject(Mobject):
use_simple_quadratic_approx: bool = False, use_simple_quadratic_approx: bool = False,
# Measured in pixel widths # Measured in pixel widths
anti_alias_width: float = 1.0, anti_alias_width: float = 1.0,
fill_border_width: float = 0.5,
use_winding_fill: bool = True, use_winding_fill: bool = True,
**kwargs **kwargs
): ):
@ -106,6 +109,7 @@ class VMobject(Mobject):
self.flat_stroke = flat_stroke self.flat_stroke = flat_stroke
self.use_simple_quadratic_approx = use_simple_quadratic_approx self.use_simple_quadratic_approx = use_simple_quadratic_approx
self.anti_alias_width = anti_alias_width self.anti_alias_width = anti_alias_width
self.fill_border_width = fill_border_width
self._use_winding_fill = use_winding_fill self._use_winding_fill = use_winding_fill
self.needs_new_triangulation = True self.needs_new_triangulation = True
@ -163,6 +167,7 @@ class VMobject(Mobject):
self.set_fill( self.set_fill(
color=self.fill_color, color=self.fill_color,
opacity=self.fill_opacity, opacity=self.fill_opacity,
border_width=self.fill_border_width,
) )
self.set_stroke( self.set_stroke(
color=self.stroke_color, color=self.stroke_color,
@ -194,9 +199,13 @@ class VMobject(Mobject):
self, self,
color: ManimColor | Iterable[ManimColor] = None, color: ManimColor | Iterable[ManimColor] = None,
opacity: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None,
border_width: float | None = None,
recurse: bool = True recurse: bool = True
): ):
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse) 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 return self
def set_stroke( def set_stroke(
@ -669,6 +678,8 @@ class VMobject(Mobject):
def append_vectorized_mobject(self, vmobject: VMobject): def append_vectorized_mobject(self, vmobject: VMobject):
self.add_subpath(vmobject.get_points()) self.add_subpath(vmobject.get_points())
n = vmobject.get_num_points()
self.data[-n:] = vmobject.data
return self return self
# #
@ -825,8 +836,31 @@ class VMobject(Mobject):
points[1] - points[0], points[1] - points[0],
points[2] - points[1], points[2] - points[1],
) )
self.data["unit_normal"][:] = normal
return 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 # Alignment
def align_points(self, vmobject: VMobject): def align_points(self, vmobject: VMobject):
winding = self._use_winding_fill and vmobject._use_winding_fill 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)] inner_tri_indices = iti[~(null1 | null2).repeat(3)]
ovi = self.get_outer_vert_indices() 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]) tri_indices = np.hstack([ovi, inner_tri_indices])
self.triangulation = tri_indices self.triangulation = tri_indices
self.needs_new_triangulation = False self.needs_new_triangulation = False
@ -1140,6 +1172,7 @@ class VMobject(Mobject):
self.refresh_triangulation() self.refresh_triangulation()
if refresh_joints: if refresh_joints:
self.get_joint_products(refresh=True) self.get_joint_products(refresh=True)
self.get_unit_normal()
return self return self
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
@ -1149,14 +1182,15 @@ class VMobject(Mobject):
return self return self
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
def reverse_points(self): def reverse_points(self, recurse: bool = True):
# This will reset which anchors are # This will reset which anchors are
# considered path ends # considered path ends
for mob in self.get_family(): for mob in self.get_family(recurse):
if not mob.has_points(): if not mob.has_points():
continue continue
inner_ends = mob.get_subpath_end_indices()[:-1] inner_ends = mob.get_subpath_end_indices()[:-1]
mob.data["point"][inner_ends + 1] = mob.data["point"][inner_ends + 2] mob.data["point"][inner_ends + 1] = mob.data["point"][inner_ends + 2]
mob.data["unit_normal"] *= -1
super().reverse_points() super().reverse_points()
return self return self
@ -1235,24 +1269,33 @@ class VMobject(Mobject):
# Build up data lists # Build up data lists
fill_datas = [] fill_datas = []
fill_border_datas = []
fill_indices = [] fill_indices = []
stroke_datas = [] stroke_datas = []
back_stroke_data = [] back_stroke_datas = []
for submob in family: for submob in family:
if submob.has_fill(): submob.get_joint_products()
submob.data["base_point"][:] = submob.data["point"][0] has_fill = submob.has_fill()
fill_datas.append(submob.data[fill_names]) 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: if self._use_winding_fill:
# Add dummy # Add dummy
fill_datas.append(submob.data[fill_names][-1:]) fill_datas.append(data[-1:])
else: else:
fill_indices.append(submob.get_triangulation()) fill_indices.append(submob.get_triangulation())
if submob.has_stroke(): # Add fill border
submob.get_joint_products() if not has_stroke:
if submob.stroke_behind: names = list(stroke_names)
lst = back_stroke_data names[names.index('stroke_rgba')] = 'fill_rgba'
else: names[names.index('stroke_width')] = 'fill_border_width'
lst = stroke_datas 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]) lst.append(submob.data[stroke_names])
# Set data array to be one longer than number of points, # Set data array to be one longer than number of points,
# with a dummy vertex added at the end. This is to ensure # 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:]) lst.append(submob.data[stroke_names][-1:])
shader_wrappers = [ 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.fill_shader_wrapper.read_in(fill_datas, fill_indices or None),
self.stroke_shader_wrapper.read_in(stroke_datas), self.stroke_shader_wrapper.read_in(stroke_datas),
] ]
# TODO, account for submob uniforms separately?
for sw in shader_wrappers: self.uniforms.update(family[0].uniforms)
# Assume uniforms of the first family member
sw.uniforms.update(family[0].get_uniforms())
sw.depth_test = family[0].depth_test
return [sw for sw in shader_wrappers if len(sw.vert_data) > 0] return [sw for sw in shader_wrappers if len(sw.vert_data) > 0]

View file

@ -288,13 +288,11 @@ class Scene(object):
def get_image(self) -> Image: def get_image(self) -> Image:
if self.window is not None: if self.window is not None:
self.window.size = self.camera.get_pixel_shape() self.camera.use_window_fbo(False)
self.window.swap_buffers() self.camera.capture(*self.mobjects)
self.update_frame()
self.window.swap_buffers()
image = self.camera.get_image() image = self.camera.get_image()
if self.window is not None: if self.window is not None:
self.window.to_default_position() self.camera.use_window_fbo(True)
return image return image
def show(self) -> None: def show(self) -> None:
@ -312,7 +310,6 @@ class Scene(object):
if self.window: if self.window:
self.window.clear() self.window.clear()
self.camera.clear()
self.camera.capture(*self.mobjects) self.camera.capture(*self.mobjects)
if self.window: if self.window:

View file

@ -7,20 +7,20 @@ import re
import OpenGL.GL as gl import OpenGL.GL as gl
import moderngl import moderngl
import numpy as np import numpy as np
from functools import lru_cache
from manimlib.utils.iterables import resize_array from manimlib.utils.iterables import resize_array
from manimlib.utils.shaders import get_shader_code_from_file from manimlib.utils.shaders import get_shader_code_from_file
from manimlib.utils.shaders import get_shader_program from manimlib.utils.shaders import get_shader_program
from manimlib.utils.shaders import image_path_to_texture from manimlib.utils.shaders import image_path_to_texture
from manimlib.utils.shaders import get_texture_id 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 manimlib.utils.shaders import release_texture
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if 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 # Mobjects that should be rendered with
@ -37,7 +37,7 @@ class ShaderWrapper(object):
vert_data: np.ndarray, vert_data: np.ndarray,
vert_indices: Optional[np.ndarray] = None, vert_indices: Optional[np.ndarray] = None,
shader_folder: Optional[str] = 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. texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures.
depth_test: bool = False, depth_test: bool = False,
render_primitive: int = moderngl.TRIANGLE_STRIP, 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_indices = (vert_indices or np.zeros(0)).astype(int)
self.vert_attributes = vert_data.dtype.names self.vert_attributes = vert_data.dtype.names
self.shader_folder = shader_folder self.shader_folder = shader_folder
self.uniforms = dict(uniforms or {}) self.uniforms: UniformDict = dict()
self.depth_test = depth_test self.depth_test = depth_test
self.render_primitive = render_primitive self.render_primitive = render_primitive
self.init_program_code() self.init_program_code()
self.init_program() self.init_program()
self.update_program_uniforms(uniforms or dict())
if texture_paths is not None: if texture_paths is not None:
self.init_textures(texture_paths) self.init_textures(texture_paths)
self.init_vao()
self.refresh_id() self.refresh_id()
self.vbo = None
self.ibo = None
self.vao = None
def init_program_code(self) -> None: def init_program_code(self) -> None:
def get_code(name: str) -> str | None: def get_code(name: str) -> str | None:
return get_shader_code_from_file( return get_shader_code_from_file(
@ -82,10 +80,16 @@ class ShaderWrapper(object):
self.vert_format = moderngl.detect_format(self.program, self.vert_attributes) self.vert_format = moderngl.detect_format(self.program, self.vert_attributes)
def init_textures(self, texture_paths: dict[str, str]): def init_textures(self, texture_paths: dict[str, str]):
for name, path in texture_paths.items(): names_to_ids = {
texture = image_path_to_texture(path, self.ctx) name: get_texture_id(image_path_to_texture(path, self.ctx))
tid = get_texture_id(texture) for name, path in texture_paths.items()
self.uniforms[name] = tid }
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): def __eq__(self, shader_wrapper: ShaderWrapper):
return all(( return all((
@ -93,7 +97,7 @@ class ShaderWrapper(object):
np.all(self.vert_indices == shader_wrapper.vert_indices), np.all(self.vert_indices == shader_wrapper.vert_indices),
self.shader_folder == shader_wrapper.shader_folder, self.shader_folder == shader_wrapper.shader_folder,
all( all(
np.all(self.uniforms[key] == shader_wrapper.uniforms[key]) self.uniforms[key] == shader_wrapper.uniforms[key]
for key in self.uniforms for key in self.uniforms
), ),
self.depth_test == shader_wrapper.depth_test, self.depth_test == shader_wrapper.depth_test,
@ -105,11 +109,7 @@ class ShaderWrapper(object):
result.ctx = self.ctx result.ctx = self.ctx
result.vert_data = self.vert_data.copy() result.vert_data = self.vert_data.copy()
result.vert_indices = self.vert_indices.copy() result.vert_indices = self.vert_indices.copy()
if self.uniforms: result.init_vao()
result.uniforms = {key: np.array(value) for key, value in self.uniforms.items()}
result.vao = None
result.vbo = None
result.ibo = None
return result return result
def is_valid(self) -> bool: def is_valid(self) -> bool:
@ -217,20 +217,23 @@ class ShaderWrapper(object):
def pre_render(self): def pre_render(self):
self.set_ctx_depth_test(self.depth_test) self.set_ctx_depth_test(self.depth_test)
self.set_ctx_clip_plane(self.use_clip_plane()) self.set_ctx_clip_plane(self.use_clip_plane())
self.update_program_uniforms()
def render(self): def render(self):
assert(self.vao is not None) assert(self.vao is not None)
self.vao.render() self.vao.render()
def update_program_uniforms(self): def update_program_uniforms(self, uniforms: UniformDict, universal: bool = False):
if self.program is None: if self.program is None:
return return
for name, value in self.uniforms.items(): for name, value in uniforms.items():
if name in self.program: if name not in self.program:
if isinstance(value, np.ndarray) and value.ndim > 0: continue
value = tuple(value) if isinstance(value, np.ndarray) and value.ndim > 0:
self.program[name].value = value 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): def get_vertex_buffer_object(self, refresh: bool = True):
if refresh: if refresh:
@ -245,8 +248,9 @@ class ShaderWrapper(object):
def generate_vao(self, refresh: bool = True): def generate_vao(self, refresh: bool = True):
self.release() self.release()
# Data buffer # Data buffer
vbo = self.get_vertex_buffer_object(refresh) vbo = self.vbo = self.get_vertex_buffer_object(refresh)
ibo = self.get_index_buffer_object(refresh) ibo = self.ibo = self.get_index_buffer_object(refresh)
# Vertex array object # Vertex array object
self.vao = self.ctx.vertex_array( self.vao = self.ctx.vertex_array(
program=self.program, program=self.program,
@ -273,7 +277,7 @@ class FillShaderWrapper(ShaderWrapper):
**kwargs **kwargs
): ):
super().__init__(ctx, *args, **kwargs) super().__init__(ctx, *args, **kwargs)
self.fill_canvas = get_fill_canvas(self.ctx)
def render(self): def render(self):
vao = self.vao vao = self.vao
@ -285,13 +289,26 @@ class FillShaderWrapper(ShaderWrapper):
return return
original_fbo = self.ctx.fbo 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() 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() 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) 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)

View file

@ -7,6 +7,7 @@ in float fill_all;
in float orientation; in float orientation;
in vec2 uv_coords; in vec2 uv_coords;
in vec3 point; in vec3 point;
in vec3 unit_normal;
out vec4 frag_color; out vec4 frag_color;
@ -14,7 +15,7 @@ out vec4 frag_color;
void main() { void main() {
if (color.a == 0) discard; 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 We want negatively oriented triangles to be canceled with positively
oriented ones. The easiest way to do this is to give them negative alpha, 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, 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 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 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); if(winding && orientation < 0) a = -a / (1 - a);
frag_color.a = a; frag_color.a = a;

View file

@ -9,11 +9,13 @@ in vec3 verts[3];
in vec4 v_color[3]; in vec4 v_color[3];
in vec3 v_base_point[3]; in vec3 v_base_point[3];
in float v_vert_index[3]; in float v_vert_index[3];
in vec3 v_unit_normal[3];
out vec4 color; out vec4 color;
out float fill_all; out float fill_all;
out float orientation; out float orientation;
out vec3 point; out vec3 point;
out vec3 unit_normal;
// uv space is where the curve coincides with y = x^2 // uv space is where the curve coincides with y = x^2
out vec2 uv_coords; out vec2 uv_coords;
@ -26,19 +28,19 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3](
// Analog of import for manim only // Analog of import for manim only
#INSERT get_gl_Position.glsl #INSERT get_gl_Position.glsl
#INSERT get_unit_normal.glsl
void emit_triangle(vec3 points[3], vec4 v_color[3]){ void emit_triangle(vec3 points[3], vec4 v_color[3]){
vec3 unit_normal = get_unit_normal(points[0], points[1], points[2]); orientation = sign(determinant(mat3(
orientation = winding ? sign(unit_normal.z) : 1.0; unit_normal,
points[1] - points[0],
points[2] - points[0]
)));
for(int i = 0; i < 3; i++){ for(int i = 0; i < 3; i++){
uv_coords = SIMPLE_QUADRATIC[i]; uv_coords = SIMPLE_QUADRATIC[i];
color = v_color[i]; color = v_color[i];
point = points[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]); gl_Position = get_gl_Position(points[i]);
EmitVertex(); EmitVertex();
} }
@ -63,7 +65,8 @@ void main(){
// the first anchor is set equal to that anchor // the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return; if (verts[0] == verts[1]) return;
vec3 mid_vert; unit_normal = v_unit_normal[1];
if(winding){ if(winding){
// Emit main triangle // Emit main triangle
fill_all = 1.0; fill_all = 1.0;

View file

@ -3,15 +3,18 @@
in vec3 point; in vec3 point;
in vec4 fill_rgba; in vec4 fill_rgba;
in vec3 base_point; in vec3 base_point;
in vec3 unit_normal;
out vec3 verts; // Bezier control point out vec3 verts; // Bezier control point
out vec4 v_color; out vec4 v_color;
out vec3 v_base_point; out vec3 v_base_point;
out vec3 v_unit_normal;
out float v_vert_index; out float v_vert_index;
void main(){ void main(){
verts = point; verts = point;
v_color = fill_rgba; v_color = fill_rgba;
v_base_point = base_point; v_base_point = base_point;
v_unit_normal = unit_normal;
v_vert_index = gl_VertexID; v_vert_index = gl_VertexID;
} }

View file

@ -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( void create_joint(
vec4 joint_product, vec4 joint_product,
vec3 unit_tan, vec3 unit_tan,
@ -78,6 +84,25 @@ void create_joint(
changing_c1 = static_c1 + shift * unit_tan; 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 // This function is responsible for finding the corners of
// a bounding region around the bezier curve, which can be // a bounding region around the bezier curve, which can be
// emitted as a triangle fan, with vertices vaguely close // emitted as a triangle fan, with vertices vaguely close
@ -95,40 +120,15 @@ void get_corners(
float aaw, float aaw,
out vec3 corners[6] out vec3 corners[6]
){ ){
bool linear = bool(is_linear);
float buff0 = 0.5 * v_stroke_width[0] + aaw; vec4 jp0 = normalized_joint_product(v_joint_product[0]);
float buff2 = 0.5 * v_stroke_width[2] + aaw; vec4 jp2 = normalized_joint_product(v_joint_product[2]);
vec3 p0_perp = get_perp(0, jp0, p0, v01, aaw);
vec4 jp0 = normalize(v_joint_product[0]); vec3 p2_perp = get_perp(2, jp2, p2, v12, aaw);
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));
}
vec3 p1_perp = 0.5 * (p0_perp + p2_perp); 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. // The order of corners should be for a triangle_strip.
vec3 c0 = p0 + p0_perp; vec3 c0 = p0 + p0_perp;
@ -139,14 +139,15 @@ void get_corners(
vec3 c5 = p2 - p2_perp; vec3 c5 = p2 - p2_perp;
// Move the inner middle control point to make // Move the inner middle control point to make
// room for the curve // room for the curve
float orientation = dot(normal0, v_joint_product[1].xyz); // float orientation = dot(unit_normal, v_joint_product[1].xyz);
if(orientation >= 0.0) c2 = 0.5 * (c0 + c4); float orientation = v_joint_product[1].z;
else if(orientation < 0.0) c3 = 0.5 * (c1 + c5); 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 // Account for previous and next control points
if(bool(flat_stroke)){ if(bool(flat_stroke)){
create_joint(jp0, v01, buff0, c1, c1, c0, c0); create_joint(jp0, v01, length(p0_perp), c1, c1, c0, c0);
create_joint(jp2, -v12, buff2, c5, c5, c4, c4); create_joint(jp2, -v12, length(p2_perp), c5, c5, c4, c4);
} }
corners = vec3[6](c0, c1, c2, c3, c4, c5); corners = vec3[6](c0, c1, c2, c3, c4, c5);
@ -167,8 +168,9 @@ void main() {
vec3 v01 = normalize(p1 - p0); vec3 v01 = normalize(p1 - p0);
vec3 v12 = normalize(p2 - p1); 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 // 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 // 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]; float sign = vec2(-1, 1)[i % 2];
// In this case, we only really care about // In this case, we only really care about
// the v coordinate // 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_anti_alias_width = scaled_aaw;
uv_stroke_width = stroke_width; uv_stroke_width = stroke_width;
}else{ }else{

View file

@ -19,6 +19,8 @@ if TYPE_CHECKING:
] ]
Selector = Union[SingleSelector, Iterable[SingleSelector]] Selector = Union[SingleSelector, Iterable[SingleSelector]]
UniformDict = Dict[str, float | bool | np.ndarray | tuple]
# These are various alternate names for np.ndarray meant to specify # These are various alternate names for np.ndarray meant to specify
# certain shapes. # certain shapes.
# #

View file

@ -133,7 +133,7 @@ def arrays_match(arr1: np.ndarray, arr2: np.ndarray) -> bool:
def array_is_constant(arr: 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: def hash_obj(obj: object) -> int:

View file

@ -9,6 +9,7 @@ import numpy as np
from manimlib.constants import DEFAULT_PIXEL_HEIGHT from manimlib.constants import DEFAULT_PIXEL_HEIGHT
from manimlib.constants import DEFAULT_PIXEL_WIDTH 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.directories import get_shader_dir
from manimlib.utils.file_ops import find_file from manimlib.utils.file_ops import find_file
@ -102,18 +103,37 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
@lru_cache() @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 Because VMobjects with fill are rendered in a funny way, using
which can display that texture as a simple quad onto a screen. 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) # Important to make sure dtype is floating point (not fixed point)
# so that alpha values can be negative and are not clipped # 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 depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used
texture_fbo = ctx.framebuffer(texture, depth_buffer) 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( simple_program = ctx.program(
vertex_shader=''' vertex_shader='''
#version 330 #version 330
@ -130,33 +150,27 @@ def get_fill_palette(ctx) -> Tuple[Framebuffer, VertexArray]:
#version 330 #version 330
uniform sampler2D Texture; uniform sampler2D Texture;
uniform float v_nudge; uniform vec3 null_rgb;
uniform float h_nudge;
in vec2 v_textcoord; in vec2 v_textcoord;
out vec4 frag_color; out vec4 color;
const float MIN_DIST_TO_NULL = 0.2;
void main() { void main() {
// Apply poor man's anti-aliasing color = texture(Texture, v_textcoord);
vec2 tc0 = v_textcoord + vec2(0, 0); if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard;
vec2 tc1 = v_textcoord + vec2(0, h_nudge);
vec2 tc2 = v_textcoord + vec2(v_nudge, 0); // Un-blend from the null value
vec2 tc3 = v_textcoord + vec2(v_nudge, h_nudge); color.rgb -= (1 - color.a) * null_rgb;
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;
//TODO, set gl_FragDepth; //TODO, set gl_FragDepth;
} }
''', ''',
) )
simple_program['Texture'].value = get_texture_id(texture) simple_program['Texture'].value = get_texture_id(texture)
# Half pixel width/height simple_program['null_rgb'].value = null_rgb
simple_program['h_nudge'].value = 0.5 / size[0]
simple_program['v_nudge'].value = 0.5 / size[1]
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
fill_texture_vao = ctx.simple_vertex_array( 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()), ctx.buffer(verts.astype('f4').tobytes()),
'texcoord', 'texcoord',
) )
return (texture_fbo, fill_texture_vao) return (texture_fbo, fill_texture_vao, null_rgb)