Skip to content

Instantly share code, notes, and snippets.

@jjrv
Last active September 25, 2024 06:24
Show Gist options
  • Save jjrv/2ed4c69aba7b571cd4ca91ce705779b2 to your computer and use it in GitHub Desktop.
Save jjrv/2ed4c69aba7b571cd4ca91ce705779b2 to your computer and use it in GitHub Desktop.
Fragment shader for variable thickness line segment with gradient fill and rounded cap
Copyright 2024- Juha Järvi
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
/** Perp dot product / cross product magnitude / 2x2 matrix determinant. */
float perpdot(vec2 a, vec2 b) {
return a.x * b.y - a.y * b.x;
}
/** SDF of the convex hull of two circles / uneven capsule,
* for extruding a line segment with linearly changing thickness.
*
* @param d B - A where A and B are vec3(x, y, radius) of each endpoint.
* @param f gl_FragCoord.xy - A.
*
* @return Signed distance to convex hull. */
float sdCirclePairHull(vec3 d, vec3 f) {
// Line segment length before extrusion, squared.
float d2 = dot(d.xy, d.xy);
// Length of external common tangent of circles around endpoints.
float tangentLen = sqrt(max(d2 - d.z * d.z, 0.0));
// Right triangle where base is the tangent line and hypotenuse is the distance between unextruded endpoints.
vec2 tangent = vec2(tangentLen, d.z);
// Rotate and scale fragcoord to a space where:
// - A is at (0, 0).
// - B is at (1, 0).
// - x axis is aligned with the unextruded line segment.
// - y axis is the distance from the line segment divided by its length
// (magnitude, to handle distance to the closest tangent line next).
vec2 aligned = vec2(
dot(f.xy, d.xy),
abs(perpdot(f.xy, d.xy))
) / d2;
// Offset of fragcoord along both tangent line segments from edge of circle around point A, to circle around point B.
// If there is no tangent line, choose the supposed endpoint with the larger radius.
float t = clamp(tangent.x == 0.0 ? sign(d.z) : dot(tangent, aligned) / tangent.x, 0.0, 1.0);
if(fract(t) == 0.0) {
// Projection of fragcoord to either tangent line segment is at an endpoint.
return length(d.xy * t - f.xy) - d.z * t;
}
// Signed distance from fragcoord to point projected on tangent line.
return perpdot(tangent, aligned);
}
/** Like for 2D canvas createRadialGradient, calculate the offset where a circle sweep
* from A to B last intersects a point.
*
* @param d B - A where A and B are vec3(x, y, radius) of each sweep endpoint.
* @param f gl_FragCoord.xy - A.
*
* @return Offset 0-1 where a circle sweep from A to B last intersects fragcoord,
* or -1 if fragcoord is outside the sweep (sdHull return value is > radius of A). */
float radialGradientOffset(vec3 d, vec3 f) {
// Proportional to distance between endpoint circles along the line segment.
// Zero if circles touch on the line segment.
float A = 2.0 * dot(d, vec3(d.xy, -d.z));
float B = 2.0 * dot(d, -f);
float C = 2.0 * dot(f, vec3(f.xy, -f.z));
// Offset along the line at the center of a circle that touches this point.
float t = -1.0;
const float epsilon = 1e-6;
if(abs(A) < epsilon) {
if(d.z * direction > 0.0 && abs(B) > epsilon) t = -0.5 * C / B;
} else {
// If negative, we're on either side outside the circle sweep.
float discriminant = B * B - A * C;
if(discriminant >= 0.0) t = (-B - sqrt(discriminant) * direction) / A;
}
return t;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment