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

Adjustments to fill antialiasing
This commit is contained in:
Grant Sanderson 2024-08-16 13:25:40 -07:00 committed by GitHub
commit 12d39ef37c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 70 additions and 55 deletions

View file

@ -49,7 +49,7 @@ class Animation(object):
self.lag_ratio = lag_ratio
self.suspend_mobject_updating = suspend_mobject_updating
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
def __str__(self) -> str:
return self.name

View file

@ -133,7 +133,7 @@ class Succession(AnimationGroup):
super().__init__(*animations, lag_ratio=lag_ratio, **kwargs)
def begin(self) -> None:
assert(len(self.animations) > 0)
assert len(self.animations) > 0
self.active_animation = self.animations[0]
self.active_animation.begin()

View file

@ -84,7 +84,7 @@ class DrawBorderThenFill(Animation):
fill_animation_config: dict = {},
**kwargs
):
assert(isinstance(vmobject, VMobject))
assert isinstance(vmobject, VMobject)
self.sm_to_index = {hash(sm): 0 for sm in vmobject.get_family()}
self.stroke_width = stroke_width
self.stroke_color = stroke_color

View file

@ -19,7 +19,7 @@ class ChangingDecimal(Animation):
suspend_mobject_updating: bool = False,
**kwargs
):
assert(isinstance(decimal_mob, DecimalNumber))
assert isinstance(decimal_mob, DecimalNumber)
self.number_update_func = number_update_func
super().__init__(
decimal_mob,

View file

@ -179,7 +179,7 @@ class ApplyMethod(Transform):
"Whoops, looks like you accidentally invoked "
"the method you want to animate"
)
assert(isinstance(method.__self__, Mobject))
assert isinstance(method.__self__, Mobject)
def create_target(self) -> Mobject:
method = self.method

View file

@ -99,7 +99,7 @@ class Camera(object):
self.light_source = Point(self.light_source_position)
def use_window_fbo(self, use: bool = True):
assert(self.window is not None)
assert self.window is not None
if use:
self.fbo = self.window_fbo
else:

View file

@ -20,12 +20,12 @@ class EventDispatcher(object):
self.draggable_object_listners: list[EventListener] = []
def add_listner(self, event_listner: EventListener):
assert(isinstance(event_listner, EventListener))
assert isinstance(event_listner, EventListener)
self.event_listners[event_listner.event_type].append(event_listner)
return self
def remove_listner(self, event_listner: EventListener):
assert(isinstance(event_listner, EventListener))
assert isinstance(event_listner, EventListener)
try:
while event_listner in self.event_listners[event_listner.event_type]:
self.event_listners[event_listner.event_type].remove(event_listner)
@ -56,7 +56,7 @@ class EventDispatcher(object):
if event_type == EventType.MouseDragEvent:
for listner in self.draggable_object_listners:
assert(isinstance(listner, EventListener))
assert isinstance(listner, EventListener)
propagate_event = listner.callback(listner.mobject, event_data)
if propagate_event is not None and propagate_event is False:
return propagate_event

View file

@ -642,6 +642,7 @@ class Arrow(Line):
end: Vect3 | Mobject,
stroke_color: ManimColor = GREY_A,
stroke_width: float = 5,
flat_stroke: bool = True,
buff: float = 0.25,
tip_width_ratio: float = 5,
tip_len_to_width: float = 0.0075,
@ -659,6 +660,7 @@ class Arrow(Line):
start, end,
stroke_color=stroke_color,
stroke_width=stroke_width,
flat_stroke=flat_stroke,
buff=buff,
**kwargs
)

View file

@ -37,7 +37,7 @@ class MotionMobject(Mobject):
"""
def __init__(self, mobject: Mobject, **kwargs):
super().__init__(**kwargs)
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
self.mobject = mobject
self.mobject.add_mouse_drag_listner(self.mob_on_mouse_drag)
# To avoid locking it as static mobject
@ -58,7 +58,7 @@ class Button(Mobject):
def __init__(self, mobject: Mobject, on_click: Callable[[Mobject]], **kwargs):
super().__init__(**kwargs)
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
self.on_click = on_click
self.mobject = mobject
self.mobject.add_mouse_press_listner(self.mob_on_mouse_press)
@ -119,7 +119,7 @@ class EnableDisableButton(ControlMobject):
self.add_mouse_press_listner(self.on_mouse_press)
def assert_value(self, value: bool) -> None:
assert(isinstance(value, bool))
assert isinstance(value, bool)
def set_value_anim(self, value: bool) -> None:
if value:
@ -168,7 +168,7 @@ class Checkbox(ControlMobject):
self.add_mouse_press_listner(self.on_mouse_press)
def assert_value(self, value: bool) -> None:
assert(isinstance(value, bool))
assert isinstance(value, bool)
def toggle_value(self) -> None:
super().set_value(not self.get_value())
@ -252,7 +252,7 @@ class LinearNumberSlider(ControlMobject):
super().__init__(value, self.bar, self.slider, self.slider_axis, **kwargs)
def assert_value(self, value: float) -> None:
assert(self.min_value <= value <= self.max_value)
assert self.min_value <= value <= self.max_value
def set_value_anim(self, value: float) -> None:
prop = (value - self.min_value) / (self.max_value - self.min_value)
@ -351,7 +351,7 @@ class ColorSliders(Group):
grid.move_to(self.selected_color_box)
for idx, square in enumerate(grid):
assert(isinstance(square, Square))
assert isinstance(square, Square)
square.set_stroke(width=0.0, opacity=0.0)
square.set_fill(colors[idx % len(colors)], 1.0)

View file

@ -110,7 +110,6 @@ class Mobject(object):
self.shader_code_replacements: dict[str, str] = dict()
self.init_data()
self._data_defaults = np.ones(1, dtype=self.data.dtype)
self.init_uniforms()
self.init_updaters()
self.init_event_listners()
@ -126,15 +125,16 @@ class Mobject(object):
return self.__class__.__name__
def __add__(self, other: Mobject) -> Mobject:
assert(isinstance(other, Mobject))
assert isinstance(other, Mobject)
return self.get_group_class()(self, other)
def __mul__(self, other: int) -> Mobject:
assert(isinstance(other, int))
assert isinstance(other, int)
return self.replicate(other)
def init_data(self, length: int = 0):
self.data = np.zeros(length, dtype=self.shader_dtype)
self._data_defaults = np.ones(1, dtype=self.data.dtype)
def init_uniforms(self):
self.uniforms: UniformDict = {
@ -228,7 +228,7 @@ class Mobject(object):
# Only these methods should directly affect points
@affects_data
def set_data(self, data: np.ndarray) -> Self:
assert(data.dtype == self.data.dtype)
assert data.dtype == self.data.dtype
self.resize_points(len(data))
self.data[:] = data
return self

View file

@ -18,9 +18,9 @@ if TYPE_CHECKING:
def assert_is_mobject_method(method):
assert(inspect.ismethod(method))
assert inspect.ismethod(method)
mobject = method.__self__
assert(isinstance(mobject, Mobject))
assert isinstance(mobject, Mobject)
def always(method, *args, **kwargs):

View file

@ -30,6 +30,7 @@ class DecimalNumber(VMobject):
color: ManimColor = WHITE,
stroke_width: float = 0,
fill_opacity: float = 1.0,
fill_border_width: float = 0.5,
num_decimal_places: int = 2,
include_sign: bool = False,
group_with_commas: bool = True,
@ -57,6 +58,7 @@ class DecimalNumber(VMobject):
color=color,
stroke_width=stroke_width,
fill_opacity=fill_opacity,
fill_border_width=fill_border_width,
**kwargs
)

View file

@ -153,7 +153,7 @@ class SampleSpace(Rectangle):
direction: np.ndarray = LEFT,
**kwargs
) -> VGroup:
assert(hasattr(self, "horizontal_parts"))
assert hasattr(self, "horizontal_parts")
parts = self.horizontal_parts
return self.get_subdivision_braces_and_labels(parts, labels, direction, **kwargs)
@ -162,7 +162,7 @@ class SampleSpace(Rectangle):
labels: str,
**kwargs
) -> VGroup:
assert(hasattr(self, "vertical_parts"))
assert hasattr(self, "vertical_parts")
parts = self.vertical_parts
return self.get_subdivision_braces_and_labels(parts, labels, UP, **kwargs)
@ -171,7 +171,7 @@ class SampleSpace(Rectangle):
labels: str,
**kwargs
) -> VGroup:
assert(hasattr(self, "vertical_parts"))
assert hasattr(self, "vertical_parts")
parts = self.vertical_parts
return self.get_subdivision_braces_and_labels(parts, labels, DOWN, **kwargs)

View file

@ -167,7 +167,7 @@ class Surface(Mobject):
b: float,
axis: int | None = None
) -> Self:
assert(isinstance(smobject, Surface))
assert isinstance(smobject, Surface)
if axis is None:
axis = self.prefered_creation_axis
if a <= 0 and b >= 1:

View file

@ -4,6 +4,7 @@ from functools import wraps
import moderngl
import numpy as np
import operator as op
from manimlib.constants import GREY_A, GREY_C, GREY_E
from manimlib.constants import BLACK
@ -101,7 +102,7 @@ class VMobject(Mobject):
use_simple_quadratic_approx: bool = False,
# Measured in pixel widths
anti_alias_width: float = 1.5,
fill_border_width: float = 0.5,
fill_border_width: float = 0.0,
use_winding_fill: bool = True,
**kwargs
):
@ -190,10 +191,13 @@ class VMobject(Mobject):
recurse: bool = True
) -> Self:
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
if border_width is None:
border_width = 0 if self.get_fill_opacity() < 1 else 0.5
for mob in self.get_family(recurse):
mob.data["fill_border_width"] = border_width
if opacity is not None and 0 < opacity < 1 and border_width is None:
border_width = 0
if border_width is not None:
self.border_width = border_width
for mob in self.get_family(recurse):
data = mob.data if mob.has_points() > 0 else mob._data_defaults
data["fill_border_width"] = border_width
self.note_changed_fill()
return self
@ -243,6 +247,7 @@ class VMobject(Mobject):
fill_color: ManimColor | Iterable[ManimColor] | None = None,
fill_opacity: float | Iterable[float] | None = None,
fill_rgba: Vect4 | None = None,
fill_border_width: float | None = None,
stroke_color: ManimColor | Iterable[ManimColor] | None = None,
stroke_opacity: float | Iterable[float] | None = None,
stroke_rgba: Vect4 | None = None,
@ -259,6 +264,7 @@ class VMobject(Mobject):
mob.set_fill(
color=fill_color,
opacity=fill_opacity,
border_width=fill_border_width,
recurse=False
)
@ -290,6 +296,7 @@ class VMobject(Mobject):
data = self.data if self.get_num_points() > 0 else self._data_defaults
return {
"fill_rgba": data['fill_rgba'].copy(),
"fill_border_width": data['fill_border_width'].copy(),
"stroke_rgba": data['stroke_rgba'].copy(),
"stroke_width": data['stroke_width'].copy(),
"stroke_background": self.stroke_behind,
@ -439,23 +446,19 @@ class VMobject(Mobject):
def apply_depth_test(
self,
anti_alias_width: float = 0,
fill_border_width: float = 0,
recurse: bool = True
) -> Self:
super().apply_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
def deactivate_depth_test(
self,
anti_alias_width: float = 1.0,
fill_border_width: float = 0.5,
recurse: bool = True
) -> Self:
super().deactivate_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
@Mobject.affects_family_data
@ -475,7 +478,7 @@ class VMobject(Mobject):
if len(anchors) == 0:
self.clear_points()
return self
assert(len(anchors) == len(handles) + 1)
assert len(anchors) == len(handles) + 1
points = resize_array(self.get_points(), 2 * len(anchors) - 1)
points[0::2] = anchors
points[1::2] = handles
@ -1059,7 +1062,7 @@ class VMobject(Mobject):
return self
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float) -> Self:
assert(isinstance(vmobject, VMobject))
assert isinstance(vmobject, VMobject)
vm_points = vmobject.get_points()
self.data["joint_product"] = vmobject.data["joint_product"]
if a <= 0 and b >= 1:
@ -1261,7 +1264,7 @@ class VMobject(Mobject):
return wrapper
def set_points(self, points: Vect3Array, refresh_joints: bool = True) -> Self:
assert(len(points) == 0 or len(points) % 2 == 1)
assert len(points) == 0 or len(points) % 2 == 1
super().set_points(points)
self.refresh_triangulation()
if refresh_joints:
@ -1271,7 +1274,7 @@ class VMobject(Mobject):
@triggers_refreshed_triangulation
def append_points(self, points: Vect3Array) -> Self:
assert(len(points) % 2 == 0)
assert len(points) % 2 == 0
super().append_points(points)
return self
@ -1393,7 +1396,12 @@ class VMobject(Mobject):
else:
fill_datas.append(submob.data[fill_names])
fill_indices.append(submob.get_triangulation())
if (not submob._has_stroke) or submob.stroke_behind:
draw_border_width = op.and_(
submob.data['fill_border_width'][0] > 0,
(not submob._has_stroke) or submob.stroke_behind,
)
if draw_border_width:
# Add fill border
submob.get_joint_products()
names = list(stroke_names)

View file

@ -333,7 +333,7 @@ class Scene(object):
self.camera.capture(*self.render_groups)
if self.window:
if self.window and not self.skip_animations:
vt = self.time - self.virtual_animation_start_time
rt = time.time() - self.real_animation_start_time
time.sleep(max(vt - rt, 0))
@ -868,7 +868,7 @@ class Scene(object):
point: Vect3,
d_point: Vect3
) -> None:
assert(self.window is not None)
assert self.window is not None
self.mouse_point.move_to(point)
event_data = {"point": point, "d_point": d_point}

View file

@ -219,7 +219,7 @@ class ShaderWrapper(object):
self.set_ctx_clip_plane(self.use_clip_plane())
def render(self):
assert(self.vao is not None)
assert self.vao is not None
self.vao.render()
def update_program_uniforms(self, camera_uniforms: UniformDict):

View file

@ -1,17 +1,18 @@
#version 330
// Value between -1 and 1
in float scaled_signed_dist_to_curve;
in float anti_alias_prop;
in float dist_to_curve;
in float half_stroke_width;
in float half_anti_alias_width;
in vec4 color;
out vec4 frag_color;
void main() {
if(anti_alias_prop < 0) discard;
if(half_stroke_width == 0) discard;
frag_color = color;
// sdf for the region around the curve we wish to color.
float signed_dist_to_region = abs(scaled_signed_dist_to_curve) - 1.0;
frag_color.a *= smoothstep(0, -anti_alias_prop, signed_dist_to_region);
float signed_dist_to_region = abs(dist_to_curve) - half_stroke_width;
frag_color.a *= smoothstep(half_anti_alias_width, -half_anti_alias_width, signed_dist_to_region);
}

View file

@ -16,8 +16,9 @@ in float v_stroke_width[3];
in vec4 v_color[3];
out vec4 color;
out float anti_alias_prop;
out float scaled_signed_dist_to_curve;
out float dist_to_curve;
out float half_stroke_width;
out float half_anti_alias_width;
// Codes for joint types
const int NO_JOINT = 0;
@ -167,8 +168,8 @@ void emit_point_with_width(
// Set styling
color = finalize_color(joint_color, point, unit_normal);
float aaw = anti_alias_width * pixel_size;
anti_alias_prop = (width == 0) ? -1.0 : 2 * aaw / (width + 2 * aaw);
half_anti_alias_width = 0.5 * anti_alias_width * pixel_size;
half_stroke_width = 0.5 * width;
// Figure out the step from the point to the corners of the
// triangle strip around the polyline
@ -178,8 +179,8 @@ void emit_point_with_width(
// The frag shader will receive a value from -1 to 1,
// reflecting where in the stroke that point is
for (int sign = -1; sign <= 1; sign += 2){
scaled_signed_dist_to_curve = sign;
emit_gl_Position(point + 0.5 * (width + aaw) * sign * step);
dist_to_curve = sign * (half_stroke_width + half_anti_alias_width);
emit_gl_Position(point + dist_to_curve * step);
EmitVertex();
}
}

View file

@ -15,7 +15,7 @@ out vec4 v_joint_product;
out float v_stroke_width;
out vec4 v_color;
const float STROKE_WIDTH_CONVERSION = 0.015;
const float STROKE_WIDTH_CONVERSION = 0.01;
void main(){
verts = point;

View file

@ -148,7 +148,8 @@ def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray]:
along with the rgb value which is meant to be discarded.
"""
cam_config = get_configuration(parse_cli())['camera_config']
size = (cam_config['pixel_width'], cam_config['pixel_height'])
# Double the size so as to effectively to 4x multi-sample antialiasing
size = (2 * cam_config['pixel_width'], 2 * cam_config['pixel_height'])
# Important to make sure dtype is floating point (not fixed point)
# so that alpha values can be negative and are not clipped