First pass at a polyline implementation for stroke

This commit is contained in:
Grant Sanderson
2024-07-31 15:51:06 -04:00
parent 5aeb457bb1
commit 3ea8393e9a
3 changed files with 94 additions and 151 deletions

View File

@ -1,66 +1,18 @@
#version 330
in vec2 uv_coords;
in float signed_dist_to_curve;
in float uv_stroke_width;
in float uv_anti_alias_width;
in vec4 color;
in float is_linear;
out vec4 frag_color;
const float QUICK_DIST_WIDTH = 0.2;
float dist_to_curve(){
// In the linear case, the curve will have
// been set to equal the x axis
if(bool(is_linear)) return abs(uv_coords.y);
// Otherwise, find the distance from uv_coords to the curve y = x^2
float x0 = uv_coords.x;
float y0 = uv_coords.y;
// This is a quick approximation for computing
// the distance to the curve.
// Evaluate F(x, y) = y - x^2
// divide by its gradient's magnitude
float Fxy = y0 - x0 * x0;
float approx_dist = abs(Fxy) * inversesqrt(1.0 + 4 * x0 * x0);
if(approx_dist < QUICK_DIST_WIDTH) return approx_dist;
// Otherwise, solve for the minimal distance.
// The distance squared between (x0, y0) and a point (x, x^2) looks like
//
// (x0 - x)^2 + (y0 - x^2)^2 = x^4 + (1 - 2y0)x^2 - 2x0 * x + (x0^2 + y0^2)
//
// Setting the derivative equal to zero (and rescaling) looks like
//
// x^3 + (0.5 - y0) * x - 0.5 * x0 = 0
//
// Adapted from https://www.shadertoy.com/view/ws3GD7
x0 = abs(x0);
float p = (0.5 - y0) / 3.0; // p / 3 in usual Cardano's formula notation
float q = 0.25 * x0; // -q / 2 in usual Cardano's formula notation
float disc = q*q + p*p*p;
float r = sqrt(abs(disc));
float x = (disc > 0.0) ?
// 1 root
pow(q + r, 1.0 / 3.0) + pow(abs(q - r), 1.0 / 3.0) * sign(-p) :
// 3 roots
2.0 * cos(atan(r, q) / 3.0) * sqrt(-p);
return length(vec2(x0 - x, y0 - x * x));
}
void main() {
if (uv_stroke_width == 0) discard;
frag_color = color;
// sdf for the region around the curve we wish to color.
float signed_dist = dist_to_curve() - 0.5 * uv_stroke_width;
frag_color.a *= smoothstep(0.5, -0.5, signed_dist / uv_anti_alias_width);
float signed_dist_to_region = abs(signed_dist_to_curve) - 0.5 * uv_stroke_width;
frag_color.a *= smoothstep(1.0, 0.0, signed_dist_to_region / uv_anti_alias_width);
frag_color.a += 0.2; // undo
}

View File

@ -1,7 +1,7 @@
#version 330
layout (triangles) in;
layout (triangle_strip, max_vertices = 6) out;
layout (triangle_strip, max_vertices = 32) out; // Related to MAX_STEPS below
uniform float anti_alias_width;
uniform float flat_stroke;
@ -17,10 +17,7 @@ in vec4 v_color[3];
out vec4 color;
out float uv_stroke_width;
out float uv_anti_alias_width;
out float is_linear;
out vec2 uv_coords;
out float signed_dist_to_curve;
// Codes for joint types
const int NO_JOINT = 0;
@ -31,7 +28,8 @@ const int MITER_JOINT = 3;
// When the cosine of the angle between
// two vectors is larger than this, we
// consider them aligned
const float COS_THRESHOLD = 0.99;
const float COS_THRESHOLD = 0.999;
const int MAX_STEPS = 16;
vec3 unit_normal = vec3(0.0, 0.0, 1.0);
@ -83,73 +81,68 @@ void create_joint(
changing_c1 = static_c1 + shift * unit_tan;
}
vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw){
vec3 get_perp(vec4 joint_product, vec3 point, vec3 tangent){
/*
Perpendicular vectors to the left of the curve
*/
float buff = 0.5 * v_stroke_width[index] + aaw;
// Add correction for sharp angles to prevent weird bevel effects
if(joint_product.w < -0.75) buff *= 4 * (joint_product.w + 1.0);
float mult = 1.0;
if(joint_product.w < -0.75) mult *= 4 * (joint_product.w + 1.0);
vec3 normal = get_joint_unit_normal(joint_product);
// Set global unit normal
unit_normal = normal;
// Choose the "outward" normal direction
if(normal.z < 0) normal *= -1;
if(bool(flat_stroke)){
return buff * normalize(cross(normal, tangent));
return mult * normalize(cross(normal, tangent));
}else{
return buff * normalize(cross(camera_position - point, tangent));
return mult * normalize(cross(camera_position - point, tangent));
}
}
// This function is responsible for finding the corners of
// a bounding region around the bezier curve, which can be
// emitted as a triangle fan, with vertices vaguely close
// to control points so that the passage of vert data to
// frag shaders is most natural.
void get_corners(
// Control points for a bezier curve
vec3 p0,
vec3 p1,
vec3 p2,
// Unit tangent vectors at p0 and p2
vec3 v01,
vec3 v12,
// Anti-alias width
float aaw,
out vec3 corners[6]
vec3 point_on_curve(float t){
return verts[0] + 2 * (verts[1] - verts[0]) * t + (verts[0] - 2 * verts[1] + verts[2]) * t * t;
}
vec3 tangent_on_curve(float t){
return 2 * (verts[1] + -verts[0]) + 2 * (verts[0] - 2 * verts[1] + verts[2]) * t;
}
void emit_point_with_width(
vec3 point,
vec3 tangent,
vec4 joint_product,
float width,
vec4 joint_color,
float aaw
){
bool linear = bool(is_linear);
vec4 jp0 = normalized_joint_product(v_joint_product[0]);
vec4 jp2 = normalized_joint_product(v_joint_product[2]);
vec3 p0_perp = get_perp(0, jp0, p0, v01, aaw);
vec3 p2_perp = get_perp(2, jp2, p2, v12, aaw);
vec3 p1_perp = 0.5 * (p0_perp + p2_perp);
if(linear){
p1_perp *= (0.5 * v_stroke_width[1] + aaw) / length(p1_perp);
vec3 unit_tan = normalize(tangent);
vec4 njp = normalized_joint_product(joint_product);
float buff = 0.5 * width + aaw;
vec3 perp = buff * get_perp(njp, point, unit_tan);
vec3 corners[2] = vec3[2](point + perp, point - perp);
create_joint(
njp, unit_tan, length(perp),
corners[0], corners[0],
corners[1], corners[1]
);
color = finalize_color(joint_color, point, unit_normal);
uv_anti_alias_width = aaw;
uv_stroke_width = width;
// Emit two corners
for(int i = 0; i < 2; i++){
float sign = i % 2 == 0 ? -1 : 1;
signed_dist_to_curve = sign * buff;
emit_gl_Position(corners[i]);
EmitVertex();
}
// The order of corners should be for a triangle_strip.
vec3 c0 = p0 + p0_perp;
vec3 c1 = p0 - p0_perp;
vec3 c2 = p1 + p1_perp;
vec3 c3 = p1 - p1_perp;
vec3 c4 = p2 + p2_perp;
vec3 c5 = p2 - p2_perp;
// Move the inner middle control point to make
// room for the curve
// float orientation = dot(unit_normal, v_joint_product[1].xyz);
float orientation = v_joint_product[1].z;
if(!linear && orientation >= 0.0) c2 = 0.5 * (c0 + c4);
else if(!linear && orientation < 0.0) c3 = 0.5 * (c1 + c5);
// Account for previous and next control points
if(bool(flat_stroke)){
create_joint(jp0, v01, length(p0_perp), c1, c1, c0, c0);
create_joint(jp2, -v12, length(p2_perp), c5, c5, c4, c4);
}
corners = vec3[6](c0, c1, c2, c3, c4, c5);
}
void main() {
@ -157,52 +150,51 @@ void main() {
// the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return;
vec3 p0 = verts[0];
vec3 p1 = verts[1];
vec3 p2 = verts[2];
vec3 v01 = normalize(p1 - p0);
vec3 v12 = normalize(p2 - p1);
vec4 jp1 = normalized_joint_product(v_joint_product[1]);
is_linear = float(jp1.w > COS_THRESHOLD);
bool is_linear = jp1.w > COS_THRESHOLD; // TODO, something with this
// We want to change the coordinates to a space where the curve
// coincides with y = x^2, between some values x0 and x2. Or, in
// the case of a linear curve just put it on the x-axis
mat4 xyz_to_uv;
float uv_scale_factor;
if(!bool(is_linear)){
bool too_steep;
xyz_to_uv = get_xyz_to_uv(p0, p1, p2, 2.0, too_steep);
is_linear = float(too_steep);
uv_scale_factor = length(xyz_to_uv[0].xyz);
// Compute subdivision
int n_steps;
if (is_linear){
n_steps = 2;
}else{
n_steps = MAX_STEPS; // TODO
}
float subdivision[MAX_STEPS];
vec3 points[MAX_STEPS];
for(int i = 0; i < MAX_STEPS; i++){
if (i >= n_steps) break;
subdivision[i] = float(i) / (n_steps - 1);
points[i] = point_on_curve(subdivision[i]);
}
// Compute joint products
vec4 joint_products[MAX_STEPS];
joint_products[0] = v_joint_product[0];
joint_products[0].xyz *= -1;
joint_products[n_steps - 1] = v_joint_product[2];
for (int i = 1; i < MAX_STEPS; i++){
if (i >= n_steps - 1) break;
vec3 v1 = points[i] - points[i - 1];
vec3 v2 = points[i + 1] - points[i];
joint_products[i].xyz = cross(v1, v2);
joint_products[i].w = dot(v1, v2);
}
// Intermediate points
float scaled_aaw = anti_alias_width * pixel_size;
vec3 corners[6];
get_corners(p0, p1, p2, v01, v12, scaled_aaw, corners);
// Emit each corner
float max_sw = max(v_stroke_width[0], v_stroke_width[2]);
for(int i = 0; i < 6; i++){
float stroke_width = v_stroke_width[i / 2];
if(bool(is_linear)){
float sign = (i % 2 == 0 ? -1 : 1);
// In this case, we only really care about
// the v coordinate
uv_coords = vec2(0, sign * (0.5 * stroke_width + scaled_aaw));
uv_anti_alias_width = scaled_aaw;
uv_stroke_width = stroke_width;
}else{
uv_coords = (xyz_to_uv * vec4(corners[i], 1.0)).xy;
uv_stroke_width = uv_scale_factor * stroke_width;
uv_anti_alias_width = uv_scale_factor * scaled_aaw;
}
color = finalize_color(v_color[i / 2], corners[i], unit_normal);
emit_gl_Position(corners[i]);
EmitVertex();
for (int i = 0; i < MAX_STEPS; i++){
if (i >= n_steps) break;
float t = subdivision[i];
emit_point_with_width(
points[i],
tangent_on_curve(t),
joint_products[i], // TODO
mix(v_stroke_width[0], v_stroke_width[2], t),
mix(v_color[0], v_color[2], t),
scaled_aaw
);
}
EndPrimitive();
}

View File

@ -20,8 +20,7 @@ const float STROKE_WIDTH_CONVERSION = 0.01;
void main(){
verts = point;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width;
v_stroke_width *= mix(frame_scale, 1, is_fixed_in_frame);
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * mix(frame_scale, 1, is_fixed_in_frame);
v_joint_product = joint_product;
v_color = stroke_rgba;
}