diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 345f578b..8a74d872 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -35,6 +35,7 @@ from manimlib.utils.space_ops import cross2d from manimlib.utils.space_ops import earclip_triangulation from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import get_unit_normal +from manimlib.utils.space_ops import normalize_along_axis from manimlib.utils.space_ops import z_to_vector from manimlib.shader_wrapper import ShaderWrapper @@ -47,6 +48,8 @@ if TYPE_CHECKING: DEFAULT_STROKE_COLOR = GREY_A DEFAULT_FILL_COLOR = GREY_C +DISJOINT_CONST = 404 + class VMobject(Mobject): n_points_per_curve: int = 3 stroke_shader_folder: str = "quadratic_bezier_stroke" @@ -58,8 +61,7 @@ class VMobject(Mobject): ] stroke_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ ("point", np.float32, (3,)), - ("prev_point", np.float32, (3,)), - ("next_point", np.float32, (3,)), + ("joint_angle", np.float32, (1,)), ("stroke_width", np.float32, (1,)), ("color", np.float32, (4,)), ] @@ -102,6 +104,7 @@ class VMobject(Mobject): self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype='i4') + self.needs_new_joint_angles = True super().__init__(**kwargs) @@ -1020,11 +1023,57 @@ class VMobject(Mobject): self.needs_new_triangulation = False return tri_indices + def refresh_joint_angles(self): + for mob in self.get_family(): + mob.needs_new_joint_angles = True + return self + + def get_joint_angles(self, result): + """ + Fills the array `result` with angle information for + each point + """ + points = self.get_points() + assert(len(result) == len(points)) + + # Unit tangent vectors + a0, h, a1 = points[0::3], points[1::3], points[2::3] + a0_to_h = normalize_along_axis(h - a0, 1) + h_to_a1 = normalize_along_axis(a1 - h, 1) + + def angles(vects1, vects2): + angle = np.arccos(np.clip((vects1 * vects2).sum(1), -1, 1)) + sgn = np.sign(cross2d(vects1, vects2)) + return sgn * angle + + # Angles at all handle points + result[1::3] = angles(a0_to_h, h_to_a1) + + # Angles at first anchors + to_a1 = np.vstack([h_to_a1[-1:], h_to_a1[:-1]]) + result[0::3] = angles(to_a1, a0_to_h) + + # Angles at second anchors + from_a2 = np.vstack([a0_to_h[1:], a0_to_h[:1]]) + result[2::3] = angles(h_to_a1, from_a2) + + # To communicate to the shader that a given anchor point + # sits at the end of a curve, we set its angle equal + # to something outside the range [-pi, pi]. + # An arbitrary constant is used + a0_matched = np.isclose(a0, np.vstack([a1[-1:], a1[:-1]])).all(1) + a1_matched = np.isclose(a1, np.vstack([a0[1:], a0[:1]])).all(1) + result[0::3][~a0_matched] = DISJOINT_CONST + result[2::3][~a1_matched] = DISJOINT_CONST + + self.needs_new_joint_angles = False + def triggers_refreshed_triangulation(func: Callable): @wraps(func) def wrapper(self, *args, **kwargs): func(self, *args, **kwargs) self.refresh_triangulation() + self.refresh_joint_angles() return wrapper @triggers_refreshed_triangulation @@ -1129,17 +1178,13 @@ class VMobject(Mobject): if len(self.stroke_data) != len(points): self.stroke_data = resize_array(self.stroke_data, len(points)) - if "points" not in self.locked_data_keys: - nppc = self.n_points_per_curve - self.stroke_data["point"] = points - self.stroke_data["prev_point"][:nppc] = points[-nppc:] - self.stroke_data["prev_point"][nppc:] = points[:-nppc] - self.stroke_data["next_point"][:-nppc] = points[nppc:] - self.stroke_data["next_point"][-nppc:] = points[:nppc] - + self.read_data_to_shader(self.stroke_data, "point", "points") self.read_data_to_shader(self.stroke_data, "color", "stroke_rgba") self.read_data_to_shader(self.stroke_data, "stroke_width", "stroke_width") + if self.needs_new_joint_angles: + self.get_joint_angles(self.stroke_data["joint_angle"][:, 0]) + return self.stroke_data def get_fill_shader_data(self) -> np.ndarray: diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 27e1d52a..4e60766b 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -21,11 +21,10 @@ uniform float gloss; uniform float shadow; in vec3 bp[3]; -in vec3 prev_bp[3]; -in vec3 next_bp[3]; -in vec4 v_color[3]; +in float v_joint_angle[3]; in float v_stroke_width[3]; +in vec4 v_color[3]; out vec4 color; out float uv_stroke_width; @@ -51,6 +50,7 @@ const float ROUND_JOINT = 1; const float BEVEL_JOINT = 2; const float MITER_JOINT = 3; const float PI = 3.141592653; +const float DISJOINT_CONST = 404.0; #INSERT quadratic_bezier_geometry_functions.glsl @@ -178,42 +178,35 @@ void set_adjascent_info(vec2 c0, vec2 tangent, } -void find_joint_info(vec2 controls[3], vec2 prev[3], vec2 next[3], int degree){ - float tol = 1e-6; - - // Made as floats not bools so they can be passed to the frag shader - has_prev = float(distance(prev[2], controls[0]) < tol); - has_next = float(distance(next[0], controls[2]) < tol); - - if(bool(has_prev)){ - vec2 tangent = controls[1] - controls[0]; - set_adjascent_info( - controls[0], tangent, degree, prev, - bevel_start, angle_from_prev - ); +void find_joint_info(){ + angle_from_prev = v_joint_angle[0]; + angle_to_next = v_joint_angle[2]; + has_prev = 1.0; + has_next = 1.0; + if(angle_from_prev == DISJOINT_CONST){ + angle_from_prev = 0.0; + has_prev = 0.0; } - if(bool(has_next)){ - vec2 tangent = controls[1] - controls[2]; - set_adjascent_info( - controls[2], tangent, degree, next, - bevel_end, angle_to_next - ); - angle_to_next *= -1; + if(angle_to_next == DISJOINT_CONST){ + angle_to_next = 0.0; + has_next = 0.0; } + bool should_bevel = ( + (joint_type == AUTO_JOINT && bezier_degree == 1.0) || + joint_type == BEVEL_JOINT + ); + bevel_start = float(should_bevel); + bevel_end = float(should_bevel); } void main() { // Convert control points to a standard form if they are linear or null vec3 controls[3]; - vec3 prev[3]; - vec3 next[3]; - unit_normal = get_unit_normal(controls); bezier_degree = get_reduced_control_points(vec3[3](bp[0], bp[1], bp[2]), controls); if(bezier_degree == 0.0) return; // Null curve int degree = int(bezier_degree); - get_reduced_control_points(vec3[3](prev_bp[0], prev_bp[1], prev_bp[2]), prev); - get_reduced_control_points(vec3[3](next_bp[0], next_bp[1], next_bp[2]), next); + unit_normal = get_unit_normal(controls); // Adjust stroke width based on distance from the camera @@ -231,13 +224,10 @@ void main() { // gets tranlated to a uv plane. The z-coordinate information will be remembered // by what's sent out to gl_Position, and by how it affects the lighting and stroke width vec2 flat_controls[3]; - vec2 flat_prev[3]; - vec2 flat_next[3]; flatten_points(controls, flat_controls); - flatten_points(prev, flat_prev); - flatten_points(next, flat_next); - find_joint_info(flat_controls, flat_prev, flat_next, degree); + // Set joint angles, etc. + find_joint_info(); // Corners of a bounding region around curve vec2 corners[5]; diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index dcb91db7..340cfda0 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -3,17 +3,15 @@ #INSERT camera_uniform_declarations.glsl in vec3 point; -in vec3 prev_point; -in vec3 next_point; +in float joint_angle; in float stroke_width; in vec4 color; // Bezier control point out vec3 bp; -out vec3 prev_bp; -out vec3 next_bp; +out float v_joint_angle; out float v_stroke_width; out vec4 v_color; @@ -23,9 +21,8 @@ const float STROKE_WIDTH_CONVERSION = 0.01; void main(){ bp = position_point_into_frame(point); - prev_bp = position_point_into_frame(prev_point); - next_bp = position_point_into_frame(next_point); v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0; + v_joint_angle = joint_angle; v_color = color; } \ No newline at end of file