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.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 *

View file

@ -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,

View file

@ -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()

View file

@ -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())

View file

@ -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,
)

View file

@ -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:

View file

@ -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):

View file

@ -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):

View file

@ -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:

View file

@ -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()

View file

@ -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]

View file

@ -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:

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

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(
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{

View file

@ -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.
#

View file

@ -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:

View file

@ -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)