From 632c35daef0e41c8888dbb76177f019ec8c66b15 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 8 Jan 2021 22:28:34 -0800 Subject: [PATCH] Fix bugs in stroke shader for 3d scenes --- manimlib/shaders/inserts/get_gl_Position.glsl | 17 +- .../quadratic_bezier_geometry_functions.glsl | 28 ++- .../shaders/quadratic_bezier_stroke/frag.glsl | 6 +- .../shaders/quadratic_bezier_stroke/geom.glsl | 177 +++++++++--------- .../shaders/quadratic_bezier_stroke/vert.glsl | 1 - 5 files changed, 129 insertions(+), 100 deletions(-) diff --git a/manimlib/shaders/inserts/get_gl_Position.glsl b/manimlib/shaders/inserts/get_gl_Position.glsl index bc7b405f..fd0e4605 100644 --- a/manimlib/shaders/inserts/get_gl_Position.glsl +++ b/manimlib/shaders/inserts/get_gl_Position.glsl @@ -5,18 +5,21 @@ const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0); +float perspective_scale_factor(float z, float focal_distance){ + // TODO, what happens when focal_distance < z? + return focal_distance / (focal_distance - z); +} + + vec4 get_gl_Position(vec3 point){ vec4 result = vec4(point, 1.0); if(!bool(is_fixed_in_frame)){ result.x *= 2.0 / frame_shape.x; result.y *= 2.0 / frame_shape.y; - result.z *= 2.0 / frame_shape.y; // Should we give the frame a z shape? Does that make sense? - result.z /= focal_distance; - result.xy /= max(1.0 - result.z, 0.0); - // Todo, does this discontinuity add weirdness? Theoretically, by this result, - // the z-coordiante of gl_Position only matter for z-indexing. The reason - // for thie line is to avoid agressive clipping of distant points. - if(result.z < 0) result.z *= 0.1; + result.xy *= perspective_scale_factor(result.z, focal_distance); + // TODO, what's the better way to do this? + // This is to keep vertices too far out of frame from getting cut. + result.z *= 0.01; } else{ result.x *= 2.0 / DEFAULT_FRAME_SHAPE.x; result.y *= 2.0 / DEFAULT_FRAME_SHAPE.y; diff --git a/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl b/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl index 7798a9d9..c0f77bb3 100644 --- a/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl +++ b/manimlib/shaders/inserts/quadratic_bezier_geometry_functions.glsl @@ -2,6 +2,26 @@ float cross2d(vec2 v, vec2 w){ return v.x * w.y - w.x * v.y; } + +mat3 get_xy_to_uv(vec2 b0, vec2 b1){ + mat3 shift = mat3( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + -b0.x, -b0.y, 1.0 + ); + + float sf = length(b1 - b0); + vec2 I = (b1 - b0) / sf; + vec2 J = vec2(-I.y, I.x); + mat3 rotate = mat3( + I.x, J.x, 0.0, + I.y, J.y, 0.0, + 0.0, 0.0, 1.0 + ); + return (1 / sf) * rotate * shift; +} + + // Orthogonal matrix to convert to a uv space defined so that // b0 goes to [0, 0] and b1 goes to [1, 0] mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){ @@ -19,10 +39,10 @@ mat4 get_xyz_to_uv(vec3 b0, vec3 b1, vec3 unit_normal){ // Transpose (hence inverse) of matrix taking // i-hat to I, k-hat to unit_normal, and j-hat to their cross mat4 rotate = mat4( - I.x, J.x, K.x, 0, - I.y, J.y, K.y, 0, - I.z, J.z, K.z, 0, - 0, 0, 0 , 1 + I.x, J.x, K.x, 0.0, + I.y, J.y, K.y, 0.0, + I.z, J.z, K.z, 0.0, + 0.0, 0.0, 0.0, 1.0 ); return (1 / scale_factor) * rotate * shift; } diff --git a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl index 08b0b3bb..cbfec6e2 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/frag.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/frag.glsl @@ -44,7 +44,9 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){ // Dist for intersection of two lines float bevel_d = max(abs(p.y), abs((rot * p).y)); // Dist for union of this intersection with the real curve - return min(dist, bevel_d); + // intersected with radius 2 away from curve to smooth out + // really sharp corners + return max(min(dist, bevel_d), dist / 2); } // Otherwise, start will be rounded off }else if(t == 1){ @@ -71,7 +73,7 @@ float modify_distance_for_endpoints(vec2 p, float dist, float t){ abs(cross2d(p - uv_b2, v21_unit)), abs(cross2d((rot * (p - uv_b2)), v21_unit)) ); - return min(dist, bevel_d); + return max(min(dist, bevel_d), dist / 2); } // Otherwise, end will be rounded off } diff --git a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl index 27764909..91c74e54 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/geom.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/geom.glsl @@ -9,6 +9,7 @@ uniform float focal_distance; uniform float is_fixed_in_frame; uniform float anti_alias_width; +uniform float flat_stroke; //Needed for lighting uniform vec3 light_source_position; @@ -45,6 +46,7 @@ const float AUTO_JOINT = 0; const float ROUND_JOINT = 1; const float BEVEL_JOINT = 2; const float MITER_JOINT = 3; +const float PI = 3.141592653; #INSERT quadratic_bezier_geometry_functions.glsl @@ -53,57 +55,46 @@ const float MITER_JOINT = 3; #INSERT add_light.glsl -void flatten_points(in vec3[3] points, out vec3[3] flat_points){ +void flatten_points(in vec3[3] points, out vec2[3] flat_points){ for(int i = 0; i < 3; i++){ - flat_points[i] = points[i]; - flat_points[i].z = 0; + float sf = perspective_scale_factor(points[i].z, focal_distance); + flat_points[i] = sf * points[i].xy; } } -float angle_between_vectors(vec3 v1, vec3 v2, vec3 normal){ +float angle_between_vectors(vec2 v1, vec2 v2){ float v1_norm = length(v1); float v2_norm = length(v2); if(v1_norm == 0 || v2_norm == 0) return 0; - vec3 nv1 = v1 / v1_norm; - vec3 nv2 = v2 / v2_norm; - // float signed_area = clamp(dot(cross(nv1, nv2), normal), -1, 1); - // return asin(signed_area); - float unsigned_angle = acos(clamp(dot(nv1, nv2), -1, 1)); - float sn = sign(dot(cross(nv1, nv2), normal)); - return sn * unsigned_angle; + float dp = dot(v1, v2) / (v1_norm * v2_norm); + float angle = acos(clamp(dp, -1.0, 1.0)); + float sn = sign(cross2d(v1, v2)); + return sn * angle; } -bool find_intersection(vec3 p0, vec3 v0, vec3 p1, vec3 v1, vec3 normal, out vec3 intersection){ +bool find_intersection(vec2 p0, vec2 v0, vec2 p1, vec2 v1, out vec2 intersection){ // Find the intersection of a line passing through // p0 in the direction v0 and one passing through p1 in // the direction p1. // That is, find a solutoin to p0 + v0 * t = p1 + v1 * s - // float det = -v0.x * v1.y + v1.x * v0.y; - float det = dot(cross(v1, v0), normal); - if(det == 0){ - // intersection = p0; - return false; - } - float t = dot(cross(p0 - p1, v1), normal) / det; + float det = -v0.x * v1.y + v1.x * v0.y; + if(det == 0) return false; + float t = cross2d(p0 - p1, v1) / det; intersection = p0 + v0 * t; return true; } -void create_joint(float angle, vec3 unit_tan, float buff, float should_bevel, - vec3 static_c0, out vec3 changing_c0, - vec3 static_c1, out vec3 changing_c1){ +void create_joint(float angle, vec2 unit_tan, float buff, + vec2 static_c0, out vec2 changing_c0, + vec2 static_c1, out vec2 changing_c1){ float shift; - bool miter = ( - (joint_type == AUTO_JOINT && abs(angle) > 2.8 && should_bevel == 1) || - (joint_type == MITER_JOINT) - ); if(abs(angle) < 1e-3){ // No joint shift = 0; - }else if(miter){ + }else if(joint_type == MITER_JOINT){ shift = buff * (-1.0 - cos(angle)) / sin(angle); }else{ // For a Bevel joint @@ -117,67 +108,64 @@ void create_joint(float angle, vec3 unit_tan, float buff, float should_bevel, // This function is responsible for finding the corners of // a bounding region around the bezier curve, which can be // emitted as a triangle fan -int get_corners(vec3 controls[3], vec3 normal, int degree, out vec3 corners[5]){ - vec3 p0 = controls[0]; - vec3 p1 = controls[1]; - vec3 p2 = controls[2]; +int get_corners(vec2 controls[3], int degree, float stroke_widths[3], out vec2 corners[5]){ + vec2 p0 = controls[0]; + vec2 p1 = controls[1]; + vec2 p2 = controls[2]; // Unit vectors for directions between control points - vec3 v10 = normalize(p0 - p1); - vec3 v12 = normalize(p2 - p1); - vec3 v01 = -v10; - vec3 v21 = -v12; + vec2 v10 = normalize(p0 - p1); + vec2 v12 = normalize(p2 - p1); + vec2 v01 = -v10; + vec2 v21 = -v12; - vec3 p0_perp = cross(normal, v01); // Pointing to the left of the curve from p0 - vec3 p2_perp = cross(normal, v12); // Pointing to the left of the curve from p2 + vec2 p0_perp = vec2(-v01.y, v01.x); // Pointing to the left of the curve from p0 + vec2 p2_perp = vec2(-v12.y, v12.x); // Pointing to the left of the curve from p2 // aaw is the added width given around the polygon for antialiasing. // In case the normal is faced away from (0, 0, 1), the vector to the // camera, this is scaled up. float aaw = anti_alias_width; - float buff0 = 0.5 * v_stroke_width[0] + aaw; - float buff2 = 0.5 * v_stroke_width[2] + aaw; + float buff0 = 0.5 * stroke_widths[0] + aaw; + float buff2 = 0.5 * stroke_widths[2] + aaw; float aaw0 = (1 - has_prev) * aaw; float aaw2 = (1 - has_next) * aaw; - vec3 c0 = p0 - buff0 * p0_perp + aaw0 * v10; - vec3 c1 = p0 + buff0 * p0_perp + aaw0 * v10; - vec3 c2 = p2 + buff2 * p2_perp + aaw2 * v12; - vec3 c3 = p2 - buff2 * p2_perp + aaw2 * v12; + vec2 c0 = p0 - buff0 * p0_perp + aaw0 * v10; + vec2 c1 = p0 + buff0 * p0_perp + aaw0 * v10; + vec2 c2 = p2 + buff2 * p2_perp + aaw2 * v12; + vec2 c3 = p2 - buff2 * p2_perp + aaw2 * v12; // Account for previous and next control points - if(has_prev > 0) create_joint(angle_from_prev, v01, buff0, bevel_start, c0, c0, c1, c1); - if(has_next > 0) create_joint(angle_to_next, v21, buff2, bevel_end, c3, c3, c2, c2); + if(has_prev > 0) create_joint(angle_from_prev, v01, buff0, c0, c0, c1, c1); + if(has_next > 0) create_joint(angle_to_next, v21, buff2, c3, c3, c2, c2); // Linear case is the simplest if(degree == 1){ // The order of corners should be for a triangle_strip. Last entry is a dummy - corners = vec3[5](c0, c1, c3, c2, vec3(0.0)); + corners = vec2[5](c0, c1, c3, c2, vec2(0.0)); return 4; } // Otherwise, form a pentagon around the curve - float orientation = sign(dot(cross(v01, v12), normal)); // Positive for ccw curves - if(orientation > 0) corners = vec3[5](c0, c1, p1, c2, c3); - else corners = vec3[5](c1, c0, p1, c3, c2); + float orientation = sign(cross2d(v01, v12)); // Positive for ccw curves + if(orientation > 0) corners = vec2[5](c0, c1, p1, c2, c3); + else corners = vec2[5](c1, c0, p1, c3, c2); // Replace corner[2] with convex hull point accounting for stroke width - find_intersection(corners[0], v01, corners[4], v21, normal, corners[2]); + find_intersection(corners[0], v01, corners[4], v21, corners[2]); return 5; } -void set_adjascent_info(vec3 c0, vec3 tangent, +void set_adjascent_info(vec2 c0, vec2 tangent, int degree, - vec3 normal, - vec3 adj[3], + vec2 adj[3], out float bevel, out float angle ){ - vec3 new_adj[3]; - float adj_degree = get_reduced_control_points(adj, new_adj); - // Check if adj_degree is zero? - angle = angle_between_vectors(c0 - new_adj[1], tangent, normal); + bool linear_adj = (angle_between_vectors(adj[1] - adj[0], adj[2] - adj[1]) < 1e-3); + angle = angle_between_vectors(c0 - adj[1], tangent); // Decide on joint type - bool one_linear = (degree == 1 || adj_degree == 1.0); + bool one_linear = (degree == 1 || linear_adj); bool should_bevel = ( (joint_type == AUTO_JOINT && one_linear) || joint_type == BEVEL_JOINT @@ -186,24 +174,24 @@ void set_adjascent_info(vec3 c0, vec3 tangent, } -void find_joint_info(vec3 controls[3], vec3 prev[3], vec3 next[3], int degree, vec3 normal){ - float tol = 1e-8; +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)){ - vec3 tangent = controls[1] - controls[0]; + vec2 tangent = controls[1] - controls[0]; set_adjascent_info( - controls[0], tangent, degree, normal, prev, + controls[0], tangent, degree, prev, bevel_start, angle_from_prev ); } if(bool(has_next)){ - vec3 tangent = controls[1] - controls[2]; + vec2 tangent = controls[1] - controls[2]; set_adjascent_info( - controls[2], tangent, degree, normal, next, + controls[2], tangent, degree, next, bevel_end, angle_to_next ); angle_to_next *= -1; @@ -212,45 +200,59 @@ void find_joint_info(vec3 controls[3], vec3 prev[3], vec3 next[3], int degree, v void main() { + // Convert control points to a standard form if they are linear or null vec3 controls[3]; + vec3 prev[3]; + vec3 next[3]; 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); + + + // Adjust stroke width based on distance from the camera + float scaled_strokes[3]; + for(int i = 0; i < 3; i++){ + float sf = perspective_scale_factor(controls[i].z, focal_distance); + if(bool(flat_stroke)){ + sf *= abs(dot(v_global_unit_normal[i], vec3(0.0, 0.0, 1.0))); + } + scaled_strokes[i] = v_stroke_width[i] * sf; + } // Control points are projected to the xy plane before drawing, which in turn // 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 - vec3 flat_controls[3]; - vec3 flat_prev[3]; - vec3 flat_next[3]; + vec2 flat_controls[3]; + vec2 flat_prev[3]; + vec2 flat_next[3]; flatten_points(controls, flat_controls); - flatten_points(vec3[3](prev_bp[0], prev_bp[1], prev_bp[2]), flat_prev); - flatten_points(vec3[3](next_bp[0], next_bp[1], next_bp[2]), flat_next); - vec3 k_hat = vec3(0.0, 0.0, 1.0); + flatten_points(prev, flat_prev); + flatten_points(next, flat_next); - // Null curve - if(degree == 0) return; - - find_joint_info(flat_controls, flat_prev, flat_next, degree, k_hat); - - // Find uv conversion matrix - mat4 xyz_to_uv = get_xyz_to_uv(flat_controls[0], flat_controls[1], k_hat); - float scale_factor = length(flat_controls[1] - flat_controls[0]); - uv_anti_alias_width = anti_alias_width / scale_factor; - uv_b2 = (xyz_to_uv * vec4(controls[2].xy, 0.0, 1.0)).xy; + find_joint_info(flat_controls, flat_prev, flat_next, degree); // Corners of a bounding region around curve - vec3 corners[5]; - int n_corners = get_corners(flat_controls, k_hat, degree, corners); + vec2 corners[5]; + int n_corners = get_corners(flat_controls, degree, scaled_strokes, corners); int index_map[5] = int[5](0, 0, 1, 2, 2); if(n_corners == 4) index_map[2] = 2; + // Find uv conversion matrix + mat3 xy_to_uv = get_xy_to_uv(flat_controls[0], flat_controls[1]); + float scale_factor = length(flat_controls[1] - flat_controls[0]); + uv_anti_alias_width = anti_alias_width / scale_factor; + uv_b2 = (xy_to_uv * vec3(flat_controls[2], 1.0)).xy; + // Emit each corner for(int i = 0; i < n_corners; i++){ - uv_coords = (xyz_to_uv * vec4(corners[i], 1.0)).xy; - uv_stroke_width = v_stroke_width[index_map[i]] / scale_factor; + uv_coords = (xy_to_uv * vec3(corners[i], 1.0)).xy; + uv_stroke_width = scaled_strokes[index_map[i]] / scale_factor; // Apply some lighting to the color before sending out. - vec3 xyz_coords = vec3(corners[i].xy, controls[index_map[i]].z); + // vec3 xyz_coords = vec3(corners[i], controls[index_map[i]].z); + vec3 xyz_coords = vec3(corners[i], controls[index_map[i]].z); color = add_light( v_color[index_map[i]], xyz_coords, @@ -259,7 +261,10 @@ void main() { gloss, shadow ); - gl_Position = get_gl_Position(xyz_coords); + gl_Position = vec4( + get_gl_Position(vec3(corners[i], 0.0)).xy, + get_gl_Position(controls[index_map[i]]).zw + ); EmitVertex(); } EndPrimitive(); diff --git a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl index ad017dd1..3b7bae20 100644 --- a/manimlib/shaders/quadratic_bezier_stroke/vert.glsl +++ b/manimlib/shaders/quadratic_bezier_stroke/vert.glsl @@ -2,7 +2,6 @@ uniform mat4 to_screen_space; uniform float is_fixed_in_frame; -uniform float focal_distance; in vec3 point; in vec3 prev_point;