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 all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- Fix the incorrect shading with non uniform markerscale in meshscatter [#3722](https://github.com/MakieOrg/Makie.jl/pull/3722)
- Add `scale_to=:flip` option to `hist`, which flips the direction of the bars [#3732](https://github.com/MakieOrg/Makie.jl/pull/3732)
- Fixed an issue with the texture atlas not updating in WGLMakie after display, causing new symbols to not show up [#3737](https://github.com/MakieOrg/Makie.jl/pull/3737)
- Added `linecap` and `joinstyle` attributes for lines and linesegments. Also normalized `miter_limit` to 60° across all backends. [#3771](https://github.com/MakieOrg/Makie.jl/pull/3771)

## [0.20.8] - 2024-02-22

Expand Down
28 changes: 23 additions & 5 deletions CairoMakie/src/primitives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,36 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio
Cairo.set_dash(ctx, pattern)
end

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

# joinstyle
miter_angle = to_value(get(primitive, :miter_limit, 2pi/3))
set_miter_limit(ctx, 2.0 * Makie.miter_angle_to_distance(miter_angle))

joinstyle = to_value(get(primitive, :joinstyle, :miter))
if joinstyle == :round
Cairo.set_line_join(ctx, Cairo.CAIRO_LINE_JOIN_ROUND)
elseif joinstyle == :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
2 changes: 1 addition & 1 deletion CairoMakie/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ excludes = Set([
"heatmaps & surface",
"Textured meshscatter", # not yet implemented
"Voxel - texture mapping", # not yet implemented
"Miter Joints for line rendering", # CairoMakie does not show overlap here and extrudes lines a little more
"Miter Joints for line rendering", # CairoMakie does not show overlap here
])

functions = [:volume, :volume!, :uv_mesh]
Expand Down
11 changes: 8 additions & 3 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 linecap;

in {{stripped_color_type}} g_color[];
in uvec2 g_id[];
Expand All @@ -29,6 +30,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;
flat out vec4 f_linepoints;
flat out vec4 f_miter_vecs;

Expand Down Expand Up @@ -78,7 +80,7 @@ void main(void)
vec2 n1 = normal_vector(v1);

// Set invalid / ignored outputs
f_truncation = vec2(-10.0); // no truncated joint
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_linepoints = vec4(-1e12);
Expand All @@ -92,13 +94,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(linecap);

// 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
55 changes: 47 additions & 8 deletions GLMakie/assets/shader/lines.frag
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ 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 {{stripped_color_type}} f_color1;
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;
flat in vec4 f_linepoints;
flat in vec4 f_miter_vecs;

Expand All @@ -39,6 +40,11 @@ uniform vec4 nan_color;

// Half width of antialiasing smoothstep
const float AA_RADIUS = 0.8;
const int BUTT = 0;
const int SQUARE = 1;
const int ROUND = 2;
const int MITER = 0;
const int BEVEL = 3;

float aastep(float threshold1, float dist) {
return smoothstep(threshold1-AA_RADIUS, threshold1+AA_RADIUS, dist);
Expand Down Expand Up @@ -127,6 +133,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 @@ -146,17 +157,45 @@ if (!debug) {
(f_quad_sdf.y > 0.0 && discard_sdf2 >= 0.0))
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_sdf.x - f_extrusion.x, f_quad_sdf.y - f_extrusion.y);
float sdf;

// f_quad_sdf.x includes everything from p1 in p2-p1 direction, i.e. >
// f_quad_sdf.y includes everything from p2 in p1-p2 direction, i.e. <
// < < | > < > < | > >
// < < 1->----<->----<-2 > >
// < < | > < > < | > >
if (f_capmode.x == ROUND) {
// in circle(p1, halfwidth) || is beyond p1 in p2-p1 direction
sdf = min(sqrt(f_quad_sdf.x * f_quad_sdf.x + f_quad_sdf.z * f_quad_sdf.z) - f_linewidth, f_quad_sdf.x);
} else if (f_capmode.x == SQUARE) {
// everything in p2-p1 direction shifted by halfwidth in p1-p2 direction (i.e. include more)
sdf = f_quad_sdf.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_sdf.x - f_extrusion.x;
// do truncate joints
sdf = max(sdf, f_truncation.x);
}

// Same as above but for p2
if (f_capmode.y == ROUND) { // rounded joint or cap
sdf = max(sdf,
min(sqrt(f_quad_sdf.y * f_quad_sdf.y + f_quad_sdf.z * f_quad_sdf.z) - f_linewidth, f_quad_sdf.y)
);
} else if (f_capmode.y == SQUARE) { // :square cap
sdf = max(sdf, f_quad_sdf.y - f_linewidth);
} else { // miter or bevel joint or :butt cap
sdf = max(sdf, f_quad_sdf.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_sdf.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)
Expand Down
47 changes: 35 additions & 12 deletions GLMakie/assets/shader/lines.geom
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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;
flat out vec4 f_linepoints;
flat out vec4 f_miter_vecs;

Expand All @@ -42,11 +43,19 @@ uniform float pattern_length;
uniform vec2 resolution;
uniform vec2 scene_origin;

uniform int linecap;
uniform int joinstyle;
uniform float miter_limit;

// Constants
const float MITER_LIMIT = -0.4;
const float AA_RADIUS = 0.8;
const float AA_THICKNESS = 4.0 * AA_RADIUS;
// NOTE: if MITER_LIMIT becomes a variable AA_THICKNESS needs to scale with the joint extrusion
const int BUTT = 0;
const int SQUARE = 1;
const int ROUND = 2;
const int MITER = 0;
const int BEVEL = 3;

vec3 screen_space(vec4 vertex) {
return vec3((0.5 * vertex.xy / vertex.w + 0.5) * resolution, vertex.z / vertex.w);
Expand Down Expand Up @@ -75,6 +84,7 @@ void emit_vertex(LineVertex vertex) {

vec2 normal_vector(in vec2 v) { return vec2(-v.y, v.x); }
vec2 normal_vector(in vec3 v) { return vec2(-v.y, v.x); }
float sign_no_zero(float value) { return value >= 0.0 ? 1.0 : -1.0; }


////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -279,17 +289,22 @@ 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
);

// 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] ? sign(dot(v0.xy, n1)) * normalize(v0.xy - v1.xy) : normalize(n0 + n1);
vec2 miter_n2 = is_truncated[1] ? sign(dot(v1.xy, n2)) * normalize(v1.xy - v2.xy) : normalize(n1 + n2);
vec2 miter = vec2(dot(v0, v1.xy), dot(v1.xy, v2));
vec2 miter_n1 = miter.x < 0.0 ?
sign_no_zero(dot(v0.xy, n1)) * normalize(v0.xy - v1.xy) : normalize(n0 + n1);
vec2 miter_n2 = miter.y < 0.0 ?
sign_no_zero(dot(v1.xy, n2)) * normalize(v1.xy - v2.xy) : normalize(n1 + n2);

// Are we truncating the joint based on miter limit or joinstyle?
// bevel / always truncate doesn't work with v1 == v2 (v0) so we use allow
// miter joints a when v1 ≈ v2 (v0)
bvec2 is_truncated = bvec2(
(joinstyle == BEVEL) ? miter.x < 0.99 : miter.x < miter_limit,
(joinstyle == BEVEL) ? miter.y < 0.99 : miter.y < miter_limit
);

// miter vectors (line vector matching miter normal)
vec2 miter_v1 = -normal_vector(miter_n1);
Expand Down Expand Up @@ -335,10 +350,12 @@ void main(void)
// '---'
// To avoid drawing the "inverted" section we move the relevant
// vertices to the crossing point (x) using this scaling factor.
vec2 shape_factor = vec2(
// TODO: skipping this for linestart/end avoid round and square being cut off
// but causes overlap...
vec2 shape_factor = (isvalid[0] && isvalid[3]) || (linecap == BUTT) ? vec2(
max(0.0, segment_length / max(segment_length, (halfwidth + AA_THICKNESS) * (extrusion[0][0] - extrusion[1][0]))), // -n
max(0.0, segment_length / max(segment_length, (halfwidth + AA_THICKNESS) * (extrusion[0][1] - extrusion[1][1]))) // +n
);
) : vec2(1.0);

// Generate static/flat outputs

Expand Down Expand Up @@ -407,6 +424,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] ? joinstyle : linecap,
isvalid[3] ? joinstyle : linecap
);

// Generate interpolated/varying outputs:

LineVertex vertex;
Expand All @@ -421,7 +444,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
2 changes: 2 additions & 0 deletions GLMakie/src/drawing_primitives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,9 @@ end
function draw_atomic(screen::Screen, scene::Scene, @nospecialize(plot::Lines))
return cached_robj!(screen, scene, plot) do gl_attributes
linestyle = pop!(gl_attributes, :linestyle)
miter_limit = pop!(gl_attributes, :miter_limit)
data = Dict{Symbol, Any}(gl_attributes)
data[:miter_limit] = map(x -> Float32(cos(pi - x)), plot, miter_limit)
positions = handle_view(plot[1], data)
data[:scene_origin] = map(plot, data[:px_per_unit], scene.viewport) do ppu, viewport
Vec2f(ppu * origin(viewport))
Expand Down
11 changes: 11 additions & 0 deletions MakieCore/src/basic_plots.jl
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ 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."
linecap = @inherit linecap
"Controls whether line joints are rounded (:round) or not (:miter)."
joinstyle = @inherit joinstyle
"Sets the minimum inner joint angle below which miter joints truncate. See also `Makie.miter_distance_to_angle()`"
miter_limit = @inherit miter_limit
"Sets which attributes to cycle when creating multiple plots."
cycle = [:color]
mixin_generic_plot_attributes()...
Expand Down Expand Up @@ -345,6 +351,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."
linecap = @inherit linecap
"Sets which attributes to cycle when creating multiple plots."
cycle = [:color]
mixin_generic_plot_attributes()...
Expand Down Expand Up @@ -581,6 +589,9 @@ Plots polygons, which are defined by
strokewidth = @inherit patchstrokewidth
"Sets the pattern of the line (e.g. `:solid`, `:dot`, `:dashdot`)"
linestyle = nothing
linecap = @inherit linecap
joinstyle = @inherit joinstyle
miter_limit = @inherit miter_limit

shading = NoShading

Expand Down