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 (GLMakie, CairoMakie) #2536

Closed
wants to merge 54 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
fe0043f
prototype linecaps in shader
ffreyer Dec 30, 2022
5c14b08
fix triangle
ffreyer Dec 30, 2022
1035f9a
fix antialiasing & add uniforms
ffreyer Dec 30, 2022
2868301
add line shortening and mirrored triangle
ffreyer Dec 30, 2022
ac54023
only shorter discontinous lines
ffreyer Dec 30, 2022
58238c4
fix printing
ffreyer Dec 30, 2022
21c260d
make linewidth and linecap_length per vertex
ffreyer Dec 30, 2022
41a200f
add linecaps to linesegments
ffreyer Dec 30, 2022
2103da7
fix cut off triangles
ffreyer Dec 30, 2022
8af7d28
minor cleanup
ffreyer Dec 30, 2022
fe26ed7
improve triangle fix
ffreyer Dec 30, 2022
df4cc82
get linestyles working
ffreyer Dec 31, 2022
0ed7fd3
add linecap attribute
ffreyer Dec 31, 2022
0bcc438
add refimg test
ffreyer Dec 31, 2022
4b3f232
update NEWS
ffreyer Dec 31, 2022
344d5ba
add linecap to CairoMakie
ffreyer Dec 31, 2022
a0e8004
update NEWS
ffreyer Dec 31, 2022
d571ae9
exclude linecap test from WGLMakie
ffreyer Dec 31, 2022
afc879a
minor cleanup
ffreyer Dec 31, 2022
1503d8f
Merge branch 'master' into ff/linecaps
ffreyer Dec 31, 2022
6ac6bf7
fix test failure
ffreyer Dec 31, 2022
5e206a9
simplify fragment shader (drop triangle caps)
ffreyer Jan 1, 2023
ff118ea
Merge branch 'master' into ff/linecaps
ffreyer Jan 1, 2023
61802c4
Merge branch 'master' into ff/linecaps
SimonDanisch Jan 2, 2023
a4ee760
remove unnecessary lastlen and maxlen
ffreyer Jan 2, 2023
7fd4efb
update recipes (linecap passthrough & tweaks)
ffreyer Jan 2, 2023
3916802
use lines without point dublication
ffreyer Jan 3, 2023
3fe7351
fix test failures
ffreyer Jan 3, 2023
fa7cbd9
update MakieLayout
ffreyer Jan 3, 2023
812ff3f
fix rebase
ffreyer Jan 3, 2023
8ff1bed
fix typo
ffreyer Jan 3, 2023
d05b002
update legendelements
ffreyer Jan 3, 2023
401e1d8
add linecap examples
ffreyer Jan 3, 2023
035ba35
mention linecap in poly docstring
ffreyer Jan 3, 2023
3da3d96
Merge branch 'master' into ff/linecaps
ffreyer Jan 3, 2023
3e92c0e
no linecap for latexstrings
ffreyer Jan 3, 2023
ef2bd88
fix typo
ffreyer Jan 3, 2023
918d268
remove linecap attribute
ffreyer Jan 3, 2023
c368811
fix convert error
ffreyer Jan 3, 2023
33f36b4
fix another typo
ffreyer Jan 3, 2023
5054952
and another one
ffreyer Jan 3, 2023
e8e0943
Merge branch 'master' into ff/linecaps
ffreyer Jan 12, 2023
11a12ba
make test name more unique
ffreyer Jan 17, 2023
a0b2a1b
Merge branch 'master' into ff/linecaps
ffreyer Jan 17, 2023
ef78de7
rework linecap_length into more general length_offset
ffreyer Jan 18, 2023
bbf3445
add reference test for offsets
ffreyer Jan 18, 2023
06afc32
add length_offset to CairoMakie
ffreyer Jan 18, 2023
29e9c1d
add offsets to WGLMakie and exclude test
ffreyer Jan 18, 2023
22e42a4
add entry for length_offset
ffreyer Jan 18, 2023
6ada0f6
remove experimental stuff
ffreyer Jan 18, 2023
e9a853b
fix screen to clip transformation
ffreyer Jan 18, 2023
835b582
Merge branch 'master' into ff/linecaps
ffreyer Jan 24, 2023
f99ccaf
Merge branch 'master' into ff/linecaps
ffreyer Jan 29, 2023
add06f8
adjust linestyle spacing with linecaps
ffreyer Jan 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions CairoMakie/src/primitives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
################################################################################

function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Union{Lines, LineSegments}))
fields = @get_attribute(primitive, (color, linewidth, linestyle))
@get_attribute(primitive, (color, linewidth, linestyle, linecap, length_offset))
linestyle = Makie.convert_attribute(linestyle, Makie.key"linestyle"())
ctx = screen.context
model = primitive[:model][]
Expand All @@ -29,6 +29,7 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio

space = to_value(get(primitive, :space, :data))
projected_positions = project_position.(Ref(scene), Ref(space), positions, Ref(model))
projected_positions = apply_line_offsets(projected_positions, length_offset, primitive)

color = to_cairo_color(color, primitive)

Expand All @@ -45,14 +46,27 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio
# Therefore, we take the diff of the given linestyle,
# to convert the "absolute" coordinates into "relative" ones.
if !isnothing(linestyle) && !(linewidth isa AbstractArray)
Cairo.set_dash(ctx, diff(Float64.(linestyle)) .* linewidth)
# linecaps are applied to every "on" segment, eating into the "off"
# segment. In GLMakie we don't do this, so we need to adjust the spacing
# here to fit the linecap without elongating dashes
capsize = linecap in (:square, :round) ? linewidth : 0.0
lengths_on_off = diff(Float64.(linestyle)) .* linewidth
lengths_on_off .+= (1 .- 2 .* (eachindex(lengths_on_off) .% 2)) .* capsize
Cairo.set_dash(ctx, lengths_on_off)
end

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
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_BUTT)
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

# we can hide the gaps by setting the line cap to round
Cairo.set_line_cap(ctx, Cairo.CAIRO_LINE_CAP_ROUND)
draw_multi(
primitive, ctx,
projected_positions,
Expand All @@ -70,6 +84,41 @@ function draw_atomic(scene::Scene, screen::Screen, @nospecialize(primitive::Unio
nothing
end

function apply_line_offsets(ps, off::Real, ::Lines)
out = copy(ps)
out[1] -= off * normalize(ps[2] - ps[1])
out[end] += off * normalize(ps[end] - ps[end-1])
return out
end

function apply_line_offsets(ps, off::Vector, ::Lines)
out = copy(ps)
out[1] -= off[1] * normalize(ps[2] - ps[1])
for i in 2:length(ps) - 1
# This relies on false * NaN = 0. Note false * number * NaN = NaN
out[i] += isnan(ps[i+1]) * (off[i] * normalize(ps[i] - ps[i-1]))
out[i] -= isnan(ps[i-1]) * (off[i] * normalize(ps[i+1] - ps[i]))
end
out[end] += off[end] * normalize(ps[end] - ps[end-1])
return out
end

function apply_line_offsets(ps, off::Real, ::LineSegments)
out = copy(ps)
for i in eachindex(ps)
out[i] += off * normalize(ps[i] - ps[i + (2(i % 2) - 1)])
end
return out
end

function apply_line_offsets(ps, off::Vector, ::LineSegments)
out = copy(ps)
for i in eachindex(ps)
out[i] += off[i] * normalize(ps[i] - ps[i + (2(i % 2) - 1)])
end
return out
end

function draw_single(primitive::Lines, ctx, positions)
n = length(positions)
@inbounds for i in 1:n
Expand Down
107 changes: 90 additions & 17 deletions GLMakie/assets/shader/line_segment.geom
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@
{{GLSL_EXTENSIONS}}

layout(lines) in;
layout(triangle_strip, max_vertices = 4) out;
layout(triangle_strip, max_vertices = 12) out;

uniform vec2 resolution;
uniform float maxlength;
uniform float thickness;
uniform float pattern_length;
uniform int linecap;

in vec4 g_color[];
in uvec2 g_id[];
in float g_thickness[];
in float g_length_offset[];

out float f_thickness;
out vec4 f_color;
out vec2 f_uv;
flat out uvec2 f_id;
flat out int f_type;

#define AA_THICKNESS 2.0
#define AA_THICKNESS 4.0
#define LINE 0
#define CIRCLE 4
#define RECTANGLE 5

vec2 screen_space(vec4 vertex)
{
Expand All @@ -32,7 +36,21 @@ void emit_vertex(vec2 position, vec2 uv, int index)
f_color = g_color[index];
gl_Position = vec4((position / resolution) * inpos.w, inpos.z, inpos.w);
f_id = g_id[index];
f_thickness = g_thickness[index] + AA_THICKNESS;
f_thickness = g_thickness[index];
f_type = LINE;
EmitVertex();
}

// for linecaps
void emit_vertex(vec2 position, vec2 uv, int index, int type)
{
vec4 inpos = gl_in[index].gl_Position;
f_uv = uv;
f_color = g_color[index];
gl_Position = vec4((position/resolution)*inpos.w, inpos.z, inpos.w);
f_id = g_id[index];
f_thickness = g_thickness[index];
f_type = type; // some cap style
EmitVertex();
}

Expand All @@ -51,18 +69,73 @@ void main(void)

float thickness_aa0 = g_thickness[0]+AA_THICKNESS;
float thickness_aa1 = g_thickness[1]+AA_THICKNESS;
// determine the direction of each of the 3 segments (previous, current, next)

// determine the direction of each of the current segment
vec2 vun0 = p1 - p0;
vec2 v0 = normalize(vun0);
// determine the normal of each of the 3 segments (previous, current, next)
float vnorm = length(vun0);
vec2 v0 = vun0 / vnorm;

// apply offset and adjust norm
p0 = p0 - 2 * g_length_offset[0] * v0;
p1 = p1 + 2 * g_length_offset[1] * v0;
vnorm = vnorm + 2 * (g_length_offset[0] + g_length_offset[1]);

// determine the normal of each of the current segment
vec2 n0 = vec2(-v0.y, v0.x);
float l = length(p1-p0);
l /= (pattern_length*10);

float uv0 = thickness_aa0/g_thickness[0];
float uv1 = thickness_aa1/g_thickness[1];
emit_vertex(p0 + thickness_aa0 * n0, vec2(0, -uv0), 0);
emit_vertex(p0 - thickness_aa0 * n0, vec2(0, uv0), 0);
emit_vertex(p1 + thickness_aa1 * n0, vec2(l, -uv1), 1);
emit_vertex(p1 - thickness_aa1 * n0, vec2(l, uv1), 1);

// Hack to remove anti-aliasing border with circle (Assuming 100 as textureSize)
float u_min = (float(linecap == CIRCLE) + 0.5) / 100.0;
// Map 0 .. pattern_length pixels to 0 .. 1 in uv space
float u_max = 0.5 * vnorm / pattern_length + u_min;

// extend line if square cap is used
vec2 linecap_gap0 = float(linecap == RECTANGLE) * g_thickness[0] * v0;
vec2 linecap_gap1 = float(linecap == RECTANGLE) * g_thickness[1] * v0;

emit_vertex(p0 - linecap_gap0 + thickness_aa0 * n0, vec2(u_min, -thickness_aa0), 0);
emit_vertex(p0 - linecap_gap0 - thickness_aa0 * n0, vec2(u_min, thickness_aa0), 0);
emit_vertex(p1 + linecap_gap1 + thickness_aa1 * n0, vec2(u_max, -thickness_aa1), 1);
emit_vertex(p1 + linecap_gap1 - thickness_aa1 * n0, vec2(u_max, thickness_aa1), 1);

// generate quads for line cap
if (linecap == CIRCLE) { // 0 doubles as no line cap
/*
Line with line caps:

cap line cap
1-----3---- ----5-----7 ^
| | | | | off_n
| p1--- ---p2 | '
| | | |
2-----4---- ----6-----8
----> off_l

1 .. 8 are the emit_vertex calls below
*/

vec2 off_n, off_l;
float duv;

// start of line segment
off_n = thickness_aa0 * n0;
off_l = thickness_aa0 * v0;
duv = 0.5 * AA_THICKNESS / g_thickness[0];

EndPrimitive();
emit_vertex(p0 + off_n - off_l, vec2(-duv, -duv), 0, linecap);
emit_vertex(p0 - off_n - off_l, vec2(-duv, 1+duv), 0, linecap);
emit_vertex(p0 + off_n, vec2(0.5, -duv), 0, linecap);
emit_vertex(p0 - off_n, vec2(0.5, 1+duv), 0, linecap);

// end of line segment
off_n = thickness_aa1 * n0;
off_l = thickness_aa1 * v0;
duv = 0.5 * AA_THICKNESS / g_thickness[1];

EndPrimitive();
emit_vertex(p1 + off_n, vec2(0.5, -duv), 1, linecap);
emit_vertex(p1 - off_n, vec2(0.5, 1+duv), 1, linecap);
emit_vertex(p1 + off_n + off_l, vec2(1+duv, -duv), 1, linecap);
emit_vertex(p1 - off_n + off_l, vec2(1+duv, 1+duv), 1, linecap);
}
}
6 changes: 5 additions & 1 deletion GLMakie/assets/shader/line_segment.vert
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ struct Nothing{ //Nothing type, to encode if some variable doesn't contain any d
bool _; //empty structs are not allowed
};

in float lastlen;
{{vertex_type}} vertex;
{{thickness_type}} thickness;
{{length_offset_type}} length_offset;

{{color_type}} color;
{{color_map_type}} color_map;
{{color_norm_type}} color_norm;

uniform mat4 projectionview, model;
uniform uint objectid;
uniform float depth_shift;

out uvec2 g_id;
out vec4 g_color;
out float g_thickness;
out float g_length_offset;

vec4 getindex(sampler2D tex, int index);
vec4 getindex(sampler1D tex, int index);
Expand All @@ -34,14 +38,14 @@ vec4 to_color(float color, sampler1D color_map, vec2 color_norm, int index){
return color_lookup(color, color_map, color_norm);
}

uniform float depth_shift;

void main()
{
int index = gl_VertexID;
g_id = uvec2(objectid, index+1);
g_color = to_color(color, color_map, color_norm, index);
g_thickness = thickness;
g_length_offset = length_offset;
gl_Position = projectionview * model * to_vec4(vertex);
gl_Position.z += gl_Position.w * depth_shift;
}
28 changes: 23 additions & 5 deletions GLMakie/assets/shader/lines.frag
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ in vec4 f_color;
in vec2 f_uv;
in float f_thickness;
flat in uvec2 f_id;
flat in int f_type; // TODO bad for performance
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the comment in the scatter shader

uniform int shape; // shape is a uniform for now. Making them a in && using them for control flow is expected to kill performance

I assume we should find a way around this?

Copy link
Collaborator Author

@ffreyer ffreyer Dec 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some benchmarking with

let 
    GLMakie.closeall()
    ps = [2 * rand(Point2f) .- 1 for _ in 1:2000]

    function myrenderloop(screen)
        yield()
        GLMakie.GLFW.SwapInterval(0)
        GLMakie.pollevents(screen)
        GLMakie.render_frame(screen)
        GLMakie.GLFW.SwapBuffers(GLMakie.to_native(screen))
        yield()

        @time for _ in 1:10_000
            GLMakie.pollevents(screen)
            GLMakie.render_frame(screen)
            GLMakie.GLFW.SwapBuffers(GLMakie.to_native(screen))
        end

        while GLMakie.isopen(screen) && !screen.stop_renderloop
            GLMakie.pollevents(screen)
            sleep(0.1)
            yield()
        end

        return
    end

    scene = Scene()
    linesegments!(scene, ps, linewidth = 10)
    screen = GLMakie.Screen(renderloop = myrenderloop)
    display(screen, scene)
end

I get the same ~5% CI got for this branch vs master.

I tried hacking together a two pass version which does the old lines in the first pass and caps in the second pass. This way we can swap out the flat in int f_type for uniform int linecap and uniform int render_pass. This ends up being about 10% slower than master though, doing both passes. Doing just the line pass is 3-4% slower.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's ok for linecaps! Can we optionally disable it and get the 5% back for high perf use cases? We could also think about having a different recipe for that though 🤷 I think the default should make good lines, and performance should be the special case!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reducing the number of options in the fragment shader (5e206a9) helped a bit ... I think. I'm never sure with gpu benchmarking. When I tested it I got a bunch of benchmarks that were ~2% slower than master and then a bunch up to 5% slower 🤷

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran them again without background processes and I'm getting ~2% now (812ff3f)

{{pattern_type}} pattern;

uniform float pattern_length;
Expand All @@ -26,11 +27,11 @@ float aastep(float threshold1, float threshold2, float value) {
}
void write2framebuffer(vec4 color, uvec2 id);

// Signed distance fields for lines
// x/y pattern
float get_sd(sampler2D pattern, vec2 uv){
return texture(pattern, uv).x;
}
uniform float maxlength;
// x pattern
vec2 get_sd(sampler1D pattern, vec2 uv){
return vec2(texture(pattern, uv.x).x, uv.y);
Expand All @@ -40,10 +41,27 @@ vec2 get_sd(Nothing _, vec2 uv){
return vec2(0.5, uv.y);
}

// Signed distance fields for caps
#define LINE 0
#define CIRCLE 4
#define RECTANGLE 5

float circle(vec2 uv){
// Radius 0.5 circle centered at (0.5, 0.5)
return 0.5-length(uv - vec2(0.5));
}


void main(){
vec2 xy = get_sd(pattern, f_uv);
float alpha = aastep(0, xy.x);
float alpha2 = aastep(-1, 1, xy.y);
vec4 color = vec4(f_color.rgb, f_color.a*alpha*alpha2);
vec4 color = vec4(f_color.rgb, 0.0);
if (f_type == CIRCLE){
float sd = f_thickness * circle(f_uv);
color = mix(color, f_color, smoothstep(-ALIASING_CONST, ALIASING_CONST, sd));
} else {
vec2 xy = get_sd(pattern, f_uv);
float alpha = aastep(0, xy.x);
float alpha2 = aastep(-f_thickness, f_thickness, xy.y);
color = vec4(f_color.rgb, f_color.a*alpha*alpha2);
}
write2framebuffer(color, f_id);
}
Loading