Track full cross product and dot product of tangent vectors at joints

And alter the convention of what flat_stroke means to be more sensible in 3d
This commit is contained in:
Grant Sanderson
2023-01-17 11:36:47 -08:00
parent 4de0d098ea
commit abbe131e8d
3 changed files with 100 additions and 82 deletions

View File

@ -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 resize_preserving_order
from manimlib.utils.iterables import arrays_match from manimlib.utils.iterables import arrays_match
from manimlib.utils.space_ops import angle_between_vectors 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 cross2d
from manimlib.utils.space_ops import earclip_triangulation from manimlib.utils.space_ops import earclip_triangulation
from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import get_norm
@ -57,13 +58,13 @@ class VMobject(Mobject):
('point', np.float32, (3,)), ('point', np.float32, (3,)),
('stroke_rgba', np.float32, (4,)), ('stroke_rgba', np.float32, (4,)),
('stroke_width', np.float32, (1,)), ('stroke_width', np.float32, (1,)),
('joint_angle', np.float32, (1,)), ('joint_product', np.float32, (4,)),
('fill_rgba', np.float32, (4,)), ('fill_rgba', np.float32, (4,)),
('orientation', np.float32, (1,)), ('orientation', np.float32, (1,)),
('vert_index', np.float32, (1,)), ('vert_index', np.float32, (1,)),
]) ])
fill_data_names = ['point', 'fill_rgba', 'orientation', 'vert_index'] 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 fill_render_primitive: int = moderngl.TRIANGLES
stroke_render_primitive: int = moderngl.TRIANGLE_STRIP stroke_render_primitive: int = moderngl.TRIANGLE_STRIP
@ -86,7 +87,7 @@ class VMobject(Mobject):
long_lines: bool = False, long_lines: bool = False,
# Could also be "no_joint", "bevel", "miter" # Could also be "no_joint", "bevel", "miter"
joint_type: str = "auto", joint_type: str = "auto",
flat_stroke: bool = False, flat_stroke: bool = True,
use_simple_quadratic_approx: bool = False, use_simple_quadratic_approx: bool = False,
# Measured in pixel widths # Measured in pixel widths
anti_alias_width: float = 1.0, anti_alias_width: float = 1.0,
@ -107,7 +108,7 @@ class VMobject(Mobject):
self.needs_new_triangulation = True self.needs_new_triangulation = True
self.triangulation = np.zeros(0, dtype='i4') 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') self.outer_vert_indices = np.zeros(0, dtype='i4')
super().__init__(**kwargs) super().__init__(**kwargs)
@ -1042,25 +1043,30 @@ class VMobject(Mobject):
self.needs_new_triangulation = False self.needs_new_triangulation = False
return tri_indices return tri_indices
def refresh_joint_angles(self): def refresh_joint_products(self):
for mob in self.get_family(): for mob in self.get_family():
mob.needs_new_joint_angles = True mob.needs_new_joint_products = True
return self return self
def get_joint_angles(self, refresh: bool = False): def recompute_joint_products(self, refresh: bool = False):
if not self.needs_new_joint_angles and not refresh: """
return self.data["joint_angle"] The 'joint product' is a 4-vector holding the cross and dot
if "joint_angle" in self.locked_data_keys: product between tangent vectors at a joint
return self.data["joint_angle"] """
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() points = self.get_points()
if(len(points) < 3): 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, h, a1 = points[0:-1:2], points[1::2], points[2::2]
a0_to_h = normalize_along_axis(h - a0, 1) a0_to_h = normalize_along_axis(h - a0, 1)
h_to_a1 = normalize_along_axis(a1 - h, 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[0:-1:2] = a0_to_h
vect_from_vert[1::2] = h_to_a1 vect_from_vert[1::2] = h_to_a1
# Joint up closed loops, or mark unclosed paths as such
ends = self.get_subpath_end_indices() ends = self.get_subpath_end_indices()
starts = [0, *(e + 2 for e in ends[:-1])] starts = [0, *(e + 2 for e in ends[:-1])]
for start, end in zip(starts, ends): for start, end in zip(starts, ends):
if start > len(a0_to_h):
continue
if self.consider_points_equal(points[start], points[end]): if self.consider_points_equal(points[start], points[end]):
vect_to_vert[start] = h_to_a1[end // 2 - 1] vect_to_vert[start] = vect_from_vert[end - 1]
vect_from_vert[end] = a0_to_h[start // 2] 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 # Compute dot and cross products
# the joint_angles array cross(
result = self.data["joint_angle"][:, 0] vect_to_vert, vect_from_vert,
result[:] = 0 out=self.data["joint_product"][:, :3]
dots = (vect_to_vert * vect_from_vert).sum(1) )
np.arccos(dots, out=result, where=((dots <= 1) & (dots >= -1))) self.data["joint_product"][:, 3] = (vect_to_vert * vect_from_vert).sum(1)
# Assumes unit normal in the positive z direction return self.data["joint_product"]
result *= np.sign(cross2d(vect_to_vert, vect_from_vert))
return result
def triggers_refreshed_triangulation(func: Callable): def triggers_refreshed_triangulation(func: Callable):
@wraps(func) @wraps(func)
@ -1098,7 +1104,7 @@ class VMobject(Mobject):
func(self, *args, **kwargs) func(self, *args, **kwargs)
if refresh: if refresh:
self.refresh_triangulation() self.refresh_triangulation()
self.refresh_joint_angles() self.refresh_joint_products()
return wrapper return wrapper
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
@ -1152,7 +1158,7 @@ class VMobject(Mobject):
def apply_points_function(self, *args, **kwargs): def apply_points_function(self, *args, **kwargs):
super().apply_points_function(*args, **kwargs) super().apply_points_function(*args, **kwargs)
self.refresh_joint_angles() self.refresh_joint_products()
# For shaders # For shaders
def init_shader_data(self): def init_shader_data(self):
@ -1203,7 +1209,7 @@ class VMobject(Mobject):
fill_datas.append(submob.data[fill_names]) fill_datas.append(submob.data[fill_names])
fill_indices.append(submob.get_triangulation()) fill_indices.append(submob.get_triangulation())
if submob.has_stroke(): if submob.has_stroke():
submob.get_joint_angles() submob.recompute_joint_products()
if submob.stroke_behind: if submob.stroke_behind:
lst = back_stroke_data lst = back_stroke_data
else: else:

View File

@ -10,7 +10,7 @@ uniform float joint_type;
in vec3 verts[3]; in vec3 verts[3];
in float v_joint_angle[3]; in vec4 v_joint_product[3];
in float v_stroke_width[3]; in float v_stroke_width[3];
in vec4 v_color[3]; in vec4 v_color[3];
in float v_vert_index[3]; in float v_vert_index[3];
@ -38,8 +38,24 @@ const float ANGLE_THRESHOLD = 1e-3;
#INSERT finalize_color.glsl #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( void create_joint(
float angle, float cos_angle,
vec3 unit_tan, vec3 unit_tan,
float buff, float buff,
vec3 static_c0, vec3 static_c0,
@ -47,16 +63,20 @@ void create_joint(
vec3 static_c1, vec3 static_c1,
out vec3 changing_c1 out vec3 changing_c1
){ ){
float shift; if(cos_angle > (1.0 - ANGLE_THRESHOLD) || int(joint_type) == NO_JOINT){
// 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){
// No joint // No joint
shift = 0; changing_c0 = static_c0;
}else if(int(joint_type) == MITER_JOINT){ changing_c1 = static_c1;
shift = buff * (-1.0 - cos(angle)) / sin(angle); 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{ }else{
// For a Bevel joint // 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_c0 = static_c0 - shift * unit_tan;
changing_c1 = static_c1 + shift * unit_tan; changing_c1 = static_c1 + shift * unit_tan;
@ -75,28 +95,35 @@ void get_corners(
// Unit tangent vectors at p0 and p2 // Unit tangent vectors at p0 and p2
vec3 v01, vec3 v01,
vec3 v12, vec3 v12,
float stroke_width0,
float stroke_width2,
// Unit normal to the whole curve
vec3 normal,
// Anti-alias width // Anti-alias width
float aaw, float aaw,
float angle_from_prev,
float angle_to_next,
out vec3 corners[6] out vec3 corners[6]
){ ){
float buff0 = 0.5 * stroke_width0 + aaw; float buff0 = 0.5 * v_stroke_width[0] + aaw;
float buff2 = 0.5 * stroke_width2 + aaw; float buff2 = 0.5 * v_stroke_width[2] + aaw;
// Add correction for sharp angles to prevent weird bevel effects (Needed?) // Add correction for sharp angles to prevent weird bevel effects
float thresh = 5 * PI / 6; if(v_joint_product[0].w < -0.5) buff0 *= -2 * v_joint_product[0].w;
if(angle_from_prev > thresh) buff0 *= 2 * sin(angle_from_prev); if(v_joint_product[2].w < -0.5) buff2 *= -2 * v_joint_product[0].w;
if(angle_to_next > thresh) buff2 *= 2 * sin(angle_to_next);
// 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 // Perpendicular vectors to the left of the curve
vec3 p0_perp = buff0 * normalize(cross(normal, v01)); vec3 p0_perp;
vec3 p2_perp = buff2 * normalize(cross(normal, v12)); 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); vec3 p1_perp = 0.5 * (p0_perp + p2_perp);
// The order of corners should be for a triangle_strip. // The order of corners should be for a triangle_strip.
@ -106,20 +133,19 @@ void get_corners(
vec3 c3 = p1 - p1_perp; vec3 c3 = p1 - p1_perp;
vec3 c4 = p2 + p2_perp; vec3 c4 = p2 + p2_perp;
vec3 c5 = p2 - p2_perp; vec3 c5 = p2 - p2_perp;
float orientation = dot(normal, cross(v01, v12));
// Move the inner middle control point to make // Move the inner middle control point to make
// room for the curve // 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); else if(orientation < 0.0) c3 = 0.5 * (c1 + c5);
// Account for previous and next control points // Account for previous and next control points
create_joint(angle_from_prev, v01, buff0, c1, c1, c0, c0); create_joint(v_joint_product[0].w, v01, buff0, c1, c1, c0, c0);
create_joint(angle_to_next, -v12, buff2, c5, c5, c4, c4); create_joint(v_joint_product[2].w, -v12, buff2, c5, c5, c4, c4);
corners = vec3[6](c0, c1, c2, c3, c4, c5); corners = vec3[6](c0, c1, c2, c3, c4, c5);
} }
void main() { void main() {
// We use the triangle strip primative, but // We use the triangle strip primative, but
// actually only need every other strip element // actually only need every other strip element
@ -129,12 +155,6 @@ void main() {
// the first anchor is set equal to that anchor // the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return; 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 p0 = verts[0];
vec3 p1 = verts[1]; vec3 p1 = verts[1];
vec3 p2 = verts[2]; vec3 p2 = verts[2];
@ -144,6 +164,7 @@ void main() {
float angle = acos(clamp(dot(v01, v12), -1, 1)); float angle = acos(clamp(dot(v01, v12), -1, 1));
is_linear = float(abs(angle) < ANGLE_THRESHOLD); is_linear = float(abs(angle) < ANGLE_THRESHOLD);
// If the curve is flat, put the middle control in the midpoint // If the curve is flat, put the middle control in the midpoint
if (bool(is_linear)) p1 = 0.5 * (p0 + p2); if (bool(is_linear)) p1 = 0.5 * (p0 + p2);
@ -158,16 +179,7 @@ void main() {
uv_anti_alias_width = uv_scale_factor * scaled_aaw; uv_anti_alias_width = uv_scale_factor * scaled_aaw;
vec3 corners[6]; vec3 corners[6];
get_corners( get_corners(p0, p1, p2, v01, v12, scaled_aaw, 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
);
// Emit each corner // Emit each corner
for(int i = 0; i < 6; i++){ for(int i = 0; i < 6; i++){
@ -177,9 +189,9 @@ void main() {
color = finalize_color( color = finalize_color(
v_color[vert_index], v_color[vert_index],
corners[i], 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(); EmitVertex();
} }
EndPrimitive(); EndPrimitive();

View File

@ -1,27 +1,27 @@
#version 330 #version 330
uniform vec2 frame_shape;
in vec3 point; in vec3 point;
in vec4 stroke_rgba; in vec4 stroke_rgba;
in float stroke_width; in float stroke_width;
in vec3 joint_normal; in vec3 joint_normal;
in float joint_angle; in vec4 joint_product;
// Bezier control point // Bezier control point
out vec3 verts; out vec3 verts;
out float v_joint_angle; out vec4 v_joint_product;
out float v_stroke_width; out float v_stroke_width;
out vec4 v_color; out vec4 v_color;
out float v_vert_index; out float v_vert_index;
const float STROKE_WIDTH_CONVERSION = 0.01; const float STROKE_WIDTH_CONVERSION = 0.01;
#INSERT get_gl_Position.glsl
void main(){ void main(){
verts = position_point_into_frame(point); verts = point;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0; 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_color = stroke_rgba;
v_vert_index = gl_VertexID; v_vert_index = gl_VertexID;
} }