diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a2cefba7..bca78fa1 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -31,6 +31,7 @@ from manimlib.utils.iterables import resize_with_interpolation from manimlib.utils.iterables import resize_preserving_order from manimlib.utils.iterables import arrays_match from manimlib.utils.space_ops import angle_between_vectors +from manimlib.utils.space_ops import cross from manimlib.utils.space_ops import cross2d from manimlib.utils.space_ops import earclip_triangulation from manimlib.utils.space_ops import get_norm @@ -57,13 +58,13 @@ class VMobject(Mobject): ('point', np.float32, (3,)), ('stroke_rgba', np.float32, (4,)), ('stroke_width', np.float32, (1,)), - ('joint_angle', np.float32, (1,)), + ('joint_product', np.float32, (4,)), ('fill_rgba', np.float32, (4,)), ('orientation', np.float32, (1,)), ('vert_index', np.float32, (1,)), ]) fill_data_names = ['point', 'fill_rgba', 'orientation', 'vert_index'] - stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_angle'] + stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product'] fill_render_primitive: int = moderngl.TRIANGLES stroke_render_primitive: int = moderngl.TRIANGLE_STRIP @@ -86,7 +87,7 @@ class VMobject(Mobject): long_lines: bool = False, # Could also be "no_joint", "bevel", "miter" joint_type: str = "auto", - flat_stroke: bool = False, + flat_stroke: bool = True, use_simple_quadratic_approx: bool = False, # Measured in pixel widths anti_alias_width: float = 1.0, @@ -107,7 +108,7 @@ class VMobject(Mobject): self.needs_new_triangulation = True self.triangulation = np.zeros(0, dtype='i4') - self.needs_new_joint_angles = True + self.needs_new_joint_products = True self.outer_vert_indices = np.zeros(0, dtype='i4') super().__init__(**kwargs) @@ -1042,25 +1043,30 @@ class VMobject(Mobject): self.needs_new_triangulation = False return tri_indices - def refresh_joint_angles(self): + def refresh_joint_products(self): for mob in self.get_family(): - mob.needs_new_joint_angles = True + mob.needs_new_joint_products = True return self - def get_joint_angles(self, refresh: bool = False): - if not self.needs_new_joint_angles and not refresh: - return self.data["joint_angle"] - if "joint_angle" in self.locked_data_keys: - return self.data["joint_angle"] + def recompute_joint_products(self, refresh: bool = False): + """ + The 'joint product' is a 4-vector holding the cross and dot + product between tangent vectors at a joint + """ + if not self.needs_new_joint_products and not refresh: + return self.data["joint_product"] - self.needs_new_joint_angles = False + if "joint_product" in self.locked_data_keys: + return self.data["joint_product"] + + self.needs_new_joint_products = False points = self.get_points() if(len(points) < 3): - return self.data["joint_angle"] + return self.data["joint_product"] - # Unit tangent vectors + # Find all the unit tangent vectors at each joint a0, h, a1 = points[0:-1:2], points[1::2], points[2::2] a0_to_h = normalize_along_axis(h - a0, 1) h_to_a1 = normalize_along_axis(a1 - h, 1) @@ -1073,24 +1079,24 @@ class VMobject(Mobject): vect_from_vert[0:-1:2] = a0_to_h vect_from_vert[1::2] = h_to_a1 + # Joint up closed loops, or mark unclosed paths as such ends = self.get_subpath_end_indices() starts = [0, *(e + 2 for e in ends[:-1])] for start, end in zip(starts, ends): - if start > len(a0_to_h): - continue if self.consider_points_equal(points[start], points[end]): - vect_to_vert[start] = h_to_a1[end // 2 - 1] - vect_from_vert[end] = a0_to_h[start // 2] + vect_to_vert[start] = vect_from_vert[end - 1] + vect_from_vert[end] = vect_to_vert[start + 1] + else: + vect_to_vert[start] = vect_from_vert[start] + vect_from_vert[end] = vect_to_vert[end] - # Compute angles, and read them into - # the joint_angles array - result = self.data["joint_angle"][:, 0] - result[:] = 0 - dots = (vect_to_vert * vect_from_vert).sum(1) - np.arccos(dots, out=result, where=((dots <= 1) & (dots >= -1))) - # Assumes unit normal in the positive z direction - result *= np.sign(cross2d(vect_to_vert, vect_from_vert)) - return result + # Compute dot and cross products + cross( + vect_to_vert, vect_from_vert, + out=self.data["joint_product"][:, :3] + ) + self.data["joint_product"][:, 3] = (vect_to_vert * vect_from_vert).sum(1) + return self.data["joint_product"] def triggers_refreshed_triangulation(func: Callable): @wraps(func) @@ -1098,7 +1104,7 @@ class VMobject(Mobject): func(self, *args, **kwargs) if refresh: self.refresh_triangulation() - self.refresh_joint_angles() + self.refresh_joint_products() return wrapper @triggers_refreshed_triangulation @@ -1152,7 +1158,7 @@ class VMobject(Mobject): def apply_points_function(self, *args, **kwargs): super().apply_points_function(*args, **kwargs) - self.refresh_joint_angles() + self.refresh_joint_products() # For shaders def init_shader_data(self): @@ -1203,7 +1209,7 @@ class VMobject(Mobject): fill_datas.append(submob.data[fill_names]) fill_indices.append(submob.get_triangulation()) if submob.has_stroke(): - submob.get_joint_angles() + submob.recompute_joint_products() if submob.stroke_behind: lst = back_stroke_data else: diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index e55bc162..73726347 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -10,7 +10,7 @@ uniform float joint_type; in vec3 verts[3]; -in float v_joint_angle[3]; +in vec4 v_joint_product[3]; in float v_stroke_width[3]; in vec4 v_color[3]; in float v_vert_index[3]; @@ -38,8 +38,24 @@ const float ANGLE_THRESHOLD = 1e-3; #INSERT finalize_color.glsl +vec3 get_joint_normal(vec4 joint_product){ + vec3 result = joint_product.xyz; + float norm = length(result); + if(norm < 1e-8){ + // If it's too short, use the middle joint angle + result = v_joint_product[1].xyz; + norm = length(result); + } + if(norm < 1e-8){ + // If that's also to short, just return unit z vector + return vec3(0.0, 0.0, 1.0); + } + return result / norm; +} + + void create_joint( - float angle, + float cos_angle, vec3 unit_tan, float buff, vec3 static_c0, @@ -47,16 +63,20 @@ void create_joint( vec3 static_c1, out vec3 changing_c1 ){ - float shift; - // if(abs(angle) < ANGLE_THRESHOLD || abs(angle) > 0.99 * PI || int(joint_type) == NO_JOINT){ - if(abs(angle) < ANGLE_THRESHOLD || int(joint_type) == NO_JOINT){ + if(cos_angle > (1.0 - ANGLE_THRESHOLD) || int(joint_type) == NO_JOINT){ // No joint - shift = 0; - }else if(int(joint_type) == MITER_JOINT){ - shift = buff * (-1.0 - cos(angle)) / sin(angle); + changing_c0 = static_c0; + changing_c1 = static_c1; + return; + } + + float shift; + float sin_angle = sqrt(1.0 - cos_angle * cos_angle); + if(int(joint_type) == MITER_JOINT){ + shift = buff * (-1.0 - cos_angle) / sin_angle; }else{ // For a Bevel joint - shift = buff * (1.0 - cos(angle)) / sin(angle); + shift = buff * (1.0 - cos_angle) / sin_angle; } changing_c0 = static_c0 - shift * unit_tan; changing_c1 = static_c1 + shift * unit_tan; @@ -75,28 +95,35 @@ void get_corners( // Unit tangent vectors at p0 and p2 vec3 v01, vec3 v12, - float stroke_width0, - float stroke_width2, - // Unit normal to the whole curve - vec3 normal, // Anti-alias width float aaw, - float angle_from_prev, - float angle_to_next, out vec3 corners[6] ){ - float buff0 = 0.5 * stroke_width0 + aaw; - float buff2 = 0.5 * stroke_width2 + aaw; + float buff0 = 0.5 * v_stroke_width[0] + aaw; + float buff2 = 0.5 * v_stroke_width[2] + aaw; - // Add correction for sharp angles to prevent weird bevel effects (Needed?) - float thresh = 5 * PI / 6; - if(angle_from_prev > thresh) buff0 *= 2 * sin(angle_from_prev); - if(angle_to_next > thresh) buff2 *= 2 * sin(angle_to_next); + // Add correction for sharp angles to prevent weird bevel effects + if(v_joint_product[0].w < -0.5) buff0 *= -2 * v_joint_product[0].w; + if(v_joint_product[2].w < -0.5) buff2 *= -2 * v_joint_product[0].w; + + // Unit normal and joint angles + vec3 normal0 = get_joint_normal(v_joint_product[0]); + vec3 normal2 = get_joint_normal(v_joint_product[2]); + // Chose the normal in the positive z direction + normal0 *= sign(normal0.z); + normal2 *= sign(normal2.z); // Perpendicular vectors to the left of the curve - vec3 p0_perp = buff0 * normalize(cross(normal, v01)); - vec3 p2_perp = buff2 * normalize(cross(normal, v12)); + vec3 p0_perp; + vec3 p2_perp; + if(bool(flat_stroke)){ + p0_perp = buff0 * normalize(cross(normal0, v01)); + p2_perp = buff2 * normalize(cross(normal2, v12)); + }else{ + p0_perp = buff0 * normal0; + p2_perp = buff2 * normal2; + } vec3 p1_perp = 0.5 * (p0_perp + p2_perp); // The order of corners should be for a triangle_strip. @@ -106,20 +133,19 @@ void get_corners( vec3 c3 = p1 - p1_perp; vec3 c4 = p2 + p2_perp; vec3 c5 = p2 - p2_perp; - float orientation = dot(normal, cross(v01, v12)); // Move the inner middle control point to make // room for the curve - if(orientation > 0.0) c2 = 0.5 * (c0 + c4); + float orientation = dot(normal0, cross(v01, v12)); + if(orientation >= 0.0) c2 = 0.5 * (c0 + c4); else if(orientation < 0.0) c3 = 0.5 * (c1 + c5); // Account for previous and next control points - create_joint(angle_from_prev, v01, buff0, c1, c1, c0, c0); - create_joint(angle_to_next, -v12, buff2, c5, c5, c4, c4); + create_joint(v_joint_product[0].w, v01, buff0, c1, c1, c0, c0); + create_joint(v_joint_product[2].w, -v12, buff2, c5, c5, c4, c4); corners = vec3[6](c0, c1, c2, c3, c4, c5); } - void main() { // We use the triangle strip primative, but // actually only need every other strip element @@ -129,12 +155,6 @@ void main() { // the first anchor is set equal to that anchor if (verts[0] == verts[1]) return; - // TODO, track true unit normal globally (probably as a uniform) - vec3 unit_normal = vec3(0.0, 0.0, 1.0); - if(bool(flat_stroke)){ - unit_normal = camera_rotation * vec3(0.0, 0.0, 1.0); - } - vec3 p0 = verts[0]; vec3 p1 = verts[1]; vec3 p2 = verts[2]; @@ -144,6 +164,7 @@ void main() { float angle = acos(clamp(dot(v01, v12), -1, 1)); is_linear = float(abs(angle) < ANGLE_THRESHOLD); + // If the curve is flat, put the middle control in the midpoint if (bool(is_linear)) p1 = 0.5 * (p0 + p2); @@ -158,16 +179,7 @@ void main() { uv_anti_alias_width = uv_scale_factor * scaled_aaw; vec3 corners[6]; - get_corners( - p0, p1, p2, v01, v12, - v_stroke_width[0], - v_stroke_width[2], - unit_normal, - scaled_aaw, - v_joint_angle[0], - v_joint_angle[2], - corners - ); + get_corners(p0, p1, p2, v01, v12, scaled_aaw, corners); // Emit each corner for(int i = 0; i < 6; i++){ @@ -177,9 +189,9 @@ void main() { color = finalize_color( v_color[vert_index], corners[i], - unit_normal + vec3(0.0, 0.0, 1.0) // TODO ); - gl_Position = get_gl_Position(corners[i]); + gl_Position = get_gl_Position(position_point_into_frame(corners[i])); EmitVertex(); } EndPrimitive(); diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index d44a4b47..e673f8dc 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -1,27 +1,27 @@ #version 330 +uniform vec2 frame_shape; + in vec3 point; in vec4 stroke_rgba; in float stroke_width; in vec3 joint_normal; -in float joint_angle; +in vec4 joint_product; // Bezier control point out vec3 verts; -out float v_joint_angle; +out vec4 v_joint_product; out float v_stroke_width; out vec4 v_color; out float v_vert_index; const float STROKE_WIDTH_CONVERSION = 0.01; -#INSERT get_gl_Position.glsl - void main(){ - verts = position_point_into_frame(point); + verts = point; v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0; - v_joint_angle = joint_angle; + v_joint_product = joint_product; v_color = stroke_rgba; v_vert_index = gl_VertexID; } \ No newline at end of file