Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add linecaps and jointstyles #3771

Merged
merged 41 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ba3b20d
add attributes
ffreyer Mar 1, 2024
8818c78
prototype linecap & linestyle in GLMakie
ffreyer Apr 4, 2024
596dada
move code around, add comments
ffreyer Apr 5, 2024
e779b97
add capstyle for linesegments
ffreyer Apr 5, 2024
8e4ceed
update WGLMakie
ffreyer Apr 5, 2024
a71b34a
revert change in padding of uncapped lines
ffreyer Apr 5, 2024
1fc2dd8
make sure truncation can't trigger
ffreyer Apr 5, 2024
f9d2a0a
update CairoMakie
ffreyer Apr 5, 2024
67eff0d
add :bevel
ffreyer Apr 5, 2024
56fa019
Merge branch 'breaking-0.21' into ff/linecaps
ffreyer Apr 5, 2024
bd3c3db
make miter_limit adjustable
ffreyer Apr 5, 2024
1663f9d
capstyle -> linecap
ffreyer Apr 5, 2024
841efe3
update changelog
ffreyer Apr 5, 2024
3242d15
add refimg tests
ffreyer Apr 7, 2024
e391cc7
add example
ffreyer Apr 7, 2024
9722fc7
fix rendering issue with bevel for continued lines
ffreyer Apr 7, 2024
500a530
use named constants
ffreyer Apr 7, 2024
c818cf7
Merge branch 'breaking-0.21' into ff/linecaps
ffreyer Apr 7, 2024
42a9833
add more space to test
ffreyer Apr 7, 2024
f458c4d
consider miter_limit in CairoMakie as well
ffreyer Apr 9, 2024
249aaad
also enable refimg test
ffreyer Apr 9, 2024
43ba4ef
switch to angle based miter_limit
ffreyer Apr 10, 2024
853153c
fix default
ffreyer Apr 10, 2024
284d5ca
tweak tests
ffreyer Apr 10, 2024
61ce6a9
note change in default miter_limit
ffreyer Apr 10, 2024
9526088
add new attributes to recipes
ffreyer Apr 10, 2024
4fde441
rename jointstyle -> joinstyle
ffreyer Apr 10, 2024
922eff3
update a few more jointstyles
ffreyer Apr 10, 2024
5b0aa34
Merge branch 'breaking-0.21' into ff/linecaps
ffreyer Apr 25, 2024
1c07498
Fix rare missing/duplicate pixels in truncated joint (#3794)
ffreyer Apr 25, 2024
766cf6d
improve truncated linecap a bit
ffreyer Apr 25, 2024
b59c508
regenerate wglmakie bundled
ffreyer Apr 25, 2024
203fb36
Merge branch 'breaking-0.21' into ff/linecaps
ffreyer Apr 25, 2024
cfd91c2
tweak shape_factor
ffreyer Apr 25, 2024
223fea0
restore file
ffreyer Apr 25, 2024
f23c76a
try fix connected sphere
ffreyer Apr 25, 2024
655c662
add debug refimgs
ffreyer Apr 25, 2024
7d6b9f9
more testing
ffreyer Apr 25, 2024
34b53ce
more testing
ffreyer Apr 25, 2024
5c37d3c
revert debugging
ffreyer Apr 25, 2024
6cfa9f7
fix test?
ffreyer Apr 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 21 additions & 5 deletions CairoMakie/src/primitives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,34 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio
Cairo.set_dash(ctx, pattern)
end

capstyle = primitive.capstyle[]
if capstyle == :square
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_SQUARE)
elseif capstyle == :round
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_ROUND)
else # :butt
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_BUTT)
end

# TODO everywhere
# Cairo.set_miter_limit(...)
# "Cairo divides the length of the miter by the line width. If the result is greater than the miter limit, the style is converted to a bevel."
jointstyle = to_value(get(primitive, :jointstyle, :miter))
if jointstyle == :round
Cairo.set_line_join(ctx, Cairo.CAIRO_LINE_JOIN_ROUND)
elseif jointstyle == :bevel
Cairo.set_line_join(ctx, Cairo.CAIRO_LINE_JOIN_BEVEL)
else # :miter
Cairo.set_line_join(ctx, Cairo.CAIRO_LINE_JOIN_MITER)
end

if primitive isa Lines && to_value(primitive.args[1]) isa BezierPath
return draw_bezierpath_lines(ctx, to_value(primitive.args[1]), primitive, color, space, model, linewidth)
end

if color isa AbstractArray || linewidth isa AbstractArray
# stroke each segment separately, this means disjointed segments with probably
# wonky dash patterns if segments are short

# Butted segments look the best for varying colors, at least when connection angles are small.
# While round style has nicer sharp joins, it looks bad with alpha colors (double paint) and
# also messes with dash patterns (they are too long because of the caps)
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_BUTT)
draw_multi(
primitive, ctx,
projected_positions,
Expand Down
15 changes: 10 additions & 5 deletions GLMakie/assets/shader/line_segment.geom
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ layout(triangle_strip, max_vertices = 4) out;
uniform vec2 resolution;
uniform float pattern_length;
{{pattern_type}} pattern;
uniform int capstyle;

in {{stripped_color_type}} g_color[];
in uvec2 g_id[];
Expand All @@ -32,6 +33,7 @@ flat out {{stripped_color_type}} f_color1;
flat out {{stripped_color_type}} f_color2;
flat out float f_alpha_weight;
flat out float f_cumulative_length;
flat out ivec2 f_capmode;

const float AA_RADIUS = 0.8;
const float AA_THICKNESS = 2.0 * AA_RADIUS;
Expand Down Expand Up @@ -79,9 +81,9 @@ void main(void)
vec2 n1 = normal_vector(v1);

// Set invalid / ignored outputs
f_quad_sdf0 = 10.0; // no joint to previous segment
f_quad_sdf2 = 10.0; // not joint to next segment
f_truncation = vec2(-10.0); // no truncated joint
f_quad_sdf0 = 1e12; // no joint to previous segment
f_quad_sdf2 = 1e12; // not joint to next segment
f_truncation = vec2(-1e12); // no truncated joint
f_pattern_overwrite = vec4(-1e12, 1.0, 1e12, 1.0); // no joints to overwrite
f_extrusion = vec2(0.5); // no joints needing extrusion
f_discard_limit = vec2(10.0); // no joints needing discards
Expand All @@ -94,13 +96,16 @@ void main(void)
f_linelength = segment_length; // and also no changes in line length
f_cumulative_length = 0.0; // resets for each new segment

// linecaps
f_capmode = ivec2(capstyle);

// Generate vertices

for (int x = 0; x < 2; x++) {
// Get offset in line direction
float v_offset = (2 * x - 1) * AA_THICKNESS;
// pass on linewidth and id (picking) for the current line vertex
float halfwidth = 0.5 * max(AA_RADIUS, g_thickness[x]);
// Get offset in line direction
float v_offset = (2 * x - 1) * (halfwidth + AA_THICKNESS);
// TODO: if we just make this a varying output we probably get var linewidths here
f_linewidth = halfwidth;
f_id = g_id[x];
Expand Down
52 changes: 43 additions & 9 deletions GLMakie/assets/shader/lines.frag
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ in vec2 f_truncation;
in float f_linestart;
in float f_linelength;

flat in float f_linewidth;
flat in float f_linewidth; // half the real linewidth here because we count from center
flat in vec4 f_pattern_overwrite;
flat in vec2 f_extrusion;
flat in vec2 f_discard_limit;
Expand All @@ -27,6 +27,7 @@ flat in {{stripped_color_type}} f_color2;
flat in float f_alpha_weight;
flat in uvec2 f_id;
flat in float f_cumulative_length;
flat in ivec2 f_capmode;

{{pattern_type}} pattern;
uniform float pattern_length;
Expand Down Expand Up @@ -128,6 +129,11 @@ float get_pattern_sdf(Nothing _, vec2 uv){

void write2framebuffer(vec4 color, uvec2 id);

// General Notes on SDFs here:
// < 0 is considered inside the shape, i.e. drawn
// min(sdf1, sdf2) is the union shapes sdf1 and sdf2
// max(sdf1, sdf2) is the intersection of sdf1 and sdf2

void main(){
vec4 color;

Expand All @@ -147,22 +153,50 @@ if (!debug) {
if (dist_in_prev < f_quad_sdf1.x || dist_in_next < f_quad_sdf1.y)
discard;

// SDF for inside vs outside along the line direction. extrusion adjusts
// the distance from p1/p2 for joints etc
float sdf = max(f_quad_sdf1.x - f_extrusion.x, f_quad_sdf1.y - f_extrusion.y);
float sdf;

// f_quad_sdf1.x includes everything from p1 in p2-p1 direction, i.e. >
// f_quad_sdf2.y includes everything from p2 in p1-p2 direction, i.e. <
// < < | > < > < | > >
// < < 1->----<->----<-2 > >
// < < | > < > < | > >
if (f_capmode.x == 2) { // rounded joint or cap
// in circle(p1, halfwidth) || is beyond p1 in p2-p1 direction
sdf = min(sqrt(f_quad_sdf1.x * f_quad_sdf1.x + f_quad_sdf1.z * f_quad_sdf1.z) - f_linewidth, f_quad_sdf1.x);
} else if (f_capmode.x == 1) { // :square cap
// everything in p2-p1 direction shifted by halfwidth in p1-p2 direction (i.e. include more)
sdf = f_quad_sdf1.x - f_linewidth;
} else { // miter or bevel joint or :butt cap
// variable shift in -(p2-p1) direction to make space for joints
sdf = f_quad_sdf1.x - f_extrusion.x;
// do truncate joints
sdf = max(sdf, f_truncation.x);
}

// Same as above but for p2
if (f_capmode.y == 2) { // rounded joint or cap
sdf = max(sdf,
min(sqrt(f_quad_sdf1.y * f_quad_sdf1.y + f_quad_sdf1.z * f_quad_sdf1.z) - f_linewidth, f_quad_sdf1.y)
);
} else if (f_capmode.y == 1) { // :square cap
sdf = max(sdf, f_quad_sdf1.y - f_linewidth);
} else { // miter or bevel joint or :butt cap
sdf = max(sdf, f_quad_sdf1.y - f_extrusion.y);
sdf = max(sdf, f_truncation.y);
}

// distance in linewidth direction
// f_quad_sdf.z is 0 along the line connecting p1 and p2 and increases along line-normal direction
// ^ | ^ ^ | ^
// 1------------2
// ^ | ^ ^ | ^
sdf = max(sdf, abs(f_quad_sdf1.z) - f_linewidth);

// outer truncation of truncated joints (smooth outside edge)
sdf = max(sdf, f_truncation.x);
sdf = max(sdf, f_truncation.y);

// inner truncation (AA for overlapping parts)
// min(a, b) keeps what is inside a and b
// where a is the smoothly cut of part just before discard triggers (i.e. visible)
// and b is the (smoothly) cut of part just after discard triggers (i.e not visible)
// 100.0x sdf makes the sdf much more sharply, avoiding overdraw in the center
// 100.0x sdf makes the sdf much more sharp, avoiding overdraw in the center
sdf = max(sdf, min(f_quad_sdf1.x + 1.0, 100.0 * (f_quad_sdf1.x - f_quad_sdf0) - 1.0));
sdf = max(sdf, min(f_quad_sdf1.y + 1.0, 100.0 * (f_quad_sdf1.y - f_quad_sdf2) - 1.0));

Expand Down
23 changes: 17 additions & 6 deletions GLMakie/assets/shader/lines.geom
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ flat out {{stripped_color_type}} f_color1;
flat out {{stripped_color_type}} f_color2;
flat out float f_alpha_weight;
flat out float f_cumulative_length;
flat out ivec2 f_capmode;

out vec3 o_view_pos;
out vec3 o_view_normal;
Expand All @@ -41,6 +42,9 @@ out vec3 o_view_normal;
uniform float pattern_length;
uniform vec2 resolution;

uniform int capstyle;
uniform int jointstyle;

// Constants
const float MITER_LIMIT = -0.4;
const float AA_RADIUS = 0.8;
Expand Down Expand Up @@ -282,18 +286,19 @@ void main(void)
vec2 n1 = normal_vector(v1);
vec2 n2 = normal_vector(v2);

// Are we truncating the joint?
bvec2 is_truncated = bvec2(
dot(v0, v1.xy) < MITER_LIMIT,
dot(v1.xy, v2) < MITER_LIMIT
);
// Are we truncating the joint based on miter limit?
bvec2 is_truncated = bvec2(dot(v0, v1.xy) < MITER_LIMIT, dot(v1.xy, v2) < MITER_LIMIT);

// Miter normals (normal of truncated edge / vector to sharp corner)
// Note: n0 + n1 = vec(0) for a 180° change in direction. +-(v0 - v1) is the
// same direction, but becomes vec(0) at 0°, so we can use it instead
vec2 miter_n1 = is_truncated[0] ? normalize(v0.xy - v1.xy) : normalize(n0 + n1);
vec2 miter_n2 = is_truncated[1] ? normalize(v1.xy - v2.xy) : normalize(n1 + n2);

// Are we truncating based on jointstyle? (bevel)
if (jointstyle == 3)
is_truncated = bvec2(isvalid[0], isvalid[1]);

// miter vectors (line vector matching miter normal)
vec2 miter_v1 = -normal_vector(miter_n1);
vec2 miter_v2 = -normal_vector(miter_n2);
Expand Down Expand Up @@ -389,6 +394,12 @@ void main(void)
// for uv's
f_cumulative_length = g_lastlen[1];

// 0 :butt/normal cap or joint | 1 :square cap | 2 rounded cap/joint
f_capmode = ivec2(
isvalid[0] ? jointstyle : capstyle,
isvalid[3] ? jointstyle : capstyle
);

// Generate interpolated/varying outputs:

LineVertex vertex;
Expand All @@ -403,7 +414,7 @@ void main(void)
if (is_truncated[x] || !isvalid[3*x]) {
// handle overlap in fragment shader via SDF comparison
offset = shape_factor[y] * (
(halfwidth * extrusion[x][y] + (2 * x - 1) * AA_THICKNESS) * v1 +
(halfwidth * max(1.0, abs(extrusion[x][y])) + AA_THICKNESS) * (2 * x - 1) * v1 +
vec3((2 * y - 1) * (halfwidth + AA_THICKNESS) * n1, 0)
);
} else {
Expand Down
6 changes: 6 additions & 0 deletions MakieCore/src/basic_plots.jl
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `posi
linewidth = @inherit linewidth
"Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`"
linestyle = nothing
"Sets the type of linecap used, i.e. :butt (flat with no extrusion), :square (flat with 1 linewidth extrusion) or :round."
capstyle = :butt
"Controls whether line joints are rounded (:round) or not (:miter)."
jointstyle = :miter
"Sets which attributes to cycle when creating multiple plots."
cycle = [:color]
mixin_generic_plot_attributes()...
Expand Down Expand Up @@ -345,6 +349,8 @@ $(Base.Docs.doc(MakieCore.generic_plot_attributes!))
linewidth = @inherit linewidth
"Sets the pattern of the line e.g. `:solid`, `:dot`, `:dashdot`. For custom patterns look at `Linestyle(Number[...])`"
linestyle = nothing
"Sets the type of linecap used, i.e. :butt (flat with no extrusion), :square (flat with 1 linewidth extrusion) or :round."
capstyle = :butt
"Sets which attributes to cycle when creating multiple plots."
cycle = [:color]
mixin_generic_plot_attributes()...
Expand Down