diff --git a/NEWS.md b/NEWS.md index a96ea390971..82fb8ef6ff7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ ## master - Changes for Bonito rename and WGLMakie docs improvements [#3477](https://github.com/MakieOrg/Makie.jl/pull/3477). +- Add stroke and glow support to scatter and text in WGLMakie [#3518](https://github.com/MakieOrg/Makie.jl/pull/3518) ## 0.20.3 diff --git a/WGLMakie/assets/sprites.frag b/WGLMakie/assets/sprites.frag index b7fdb220cd0..97e04d8fff8 100644 --- a/WGLMakie/assets/sprites.frag +++ b/WGLMakie/assets/sprites.frag @@ -12,6 +12,12 @@ in vec2 frag_uv; // Half width of antialiasing smoothstep #define ANTIALIAS_RADIUS 0.8 + +in float frag_uvscale; +in float frag_distancefield_scale; +in vec4 frag_uv_offset_width; +flat in uint frag_instance_id; + // These versions of aastep assume that `dist` is a signed distance function // which has been scaled to be in units of pixels. float aastep(float threshold1, float dist) { @@ -58,9 +64,23 @@ void fill(sampler2D image, vec4 fillcolor, vec2 uv, float infill, inout vec4 col color = mix(color, im_color, infill); } -in float frag_uvscale; -in float frag_distancefield_scale; -in vec4 frag_uv_offset_width; +void stroke(vec4 strokecolor, float signed_distance, float width, inout vec4 color){ + if (width != 0.0){ + float t = aastep(min(width, 0.0), max(width, 0.0), signed_distance); + vec4 bg_color = mix(color, vec4(strokecolor.rgb, 0), float(signed_distance < 0.5 * width)); + color = mix(bg_color, strokecolor, t); + } +} + +void glow(vec4 glowcolor, float signed_distance, float inside, inout vec4 color){ + float glow_width = get_glowwidth(); + float stroke_width = get_strokewidth(); + if (glow_width > 0.0){ + float outside = (abs(signed_distance) - stroke_width) / glow_width; + float alpha = 1.0 - outside; + color = mix(vec4(glowcolor.rgb, glowcolor.a*alpha), color, inside); + } +} float scaled_distancefield(sampler2D distancefield, vec2 uv){ // Glyph distance field units are in pixels. Convert to same distance @@ -73,7 +93,6 @@ float scaled_distancefield(bool distancefield, vec2 uv){ return 0.0; } -flat in uint frag_instance_id; vec4 pack_int(uint id, uint index) { vec4 unpack; unpack.x = float((id & uint(0xff00)) >> 8) / 255.0; @@ -84,16 +103,24 @@ vec4 pack_int(uint id, uint index) { } void main() { - - int shape = get_shape_type(); float signed_distance = 0.0; + vec4 uv_off = frag_uv_offset_width; vec2 tex_uv = mix(uv_off.xy, uv_off.zw, clamp(frag_uv, 0.0, 1.0)); + + int shape = get_shape_type(); if(shape == CIRCLE) signed_distance = circle(frag_uv); - else if(shape == DISTANCEFIELD) + else if(shape == DISTANCEFIELD) { signed_distance = scaled_distancefield(distancefield, tex_uv); - else if(shape == ROUNDED_RECTANGLE) + if (get_strokewidth() > 0.0 || get_glowwidth() > 0.0) { + // Compensate for the clamping of tex_uv by an approximate + // extension of the signed distance outside the valid texture + // region. + vec2 bufuv = frag_uv - clamp(frag_uv, 0.0, 1.0); + signed_distance -= length(bufuv); + } + } else if(shape == ROUNDED_RECTANGLE) signed_distance = rounded_rectangle(frag_uv, vec2(0.2), vec2(0.8)); else if(shape == RECTANGLE) signed_distance = 1.0; // rectangle(f_uv); @@ -101,15 +128,32 @@ void main() { signed_distance = triangle(frag_uv); signed_distance *= frag_uvscale; - float inside = aastep(0.0, signed_distance); + + + float stroke_width = get_strokewidth(); + float inside_start = max(-stroke_width, 0.0); + float inside = aastep(inside_start, signed_distance); + vec4 final_color = vec4(frag_color.xyz, 0); fill(image, frag_color, frag_uv, inside, final_color); + stroke(get_strokecolor(), signed_distance, -stroke_width, final_color); + glow(get_glowcolor(), signed_distance, aastep(-stroke_width, signed_distance), final_color); + + // debug - show background + // final_color.a = clamp(final_color.a, 0.0, 1.0); + // final_color = vec4( + // vec3(1,0,0) * (1.0 - final_color.a) + final_color.rgb * final_color.a, + // 0.4 + 0.6 * final_color.a + // ); + if (picking) { if (final_color.a > 0.1) { fragment_color = pack_int(object_id, frag_instance_id); } return; } + + if (final_color.a <= 0.0){ discard; } diff --git a/WGLMakie/assets/sprites.vert b/WGLMakie/assets/sprites.vert index 35f8eaedd86..e06dee067e1 100644 --- a/WGLMakie/assets/sprites.vert +++ b/WGLMakie/assets/sprites.vert @@ -9,6 +9,10 @@ out float frag_uvscale; out float frag_distancefield_scale; out vec4 frag_uv_offset_width; +flat out uint frag_instance_id; + +#define ANTIALIAS_RADIUS 0.8 + mat4 qmat(vec4 quat){ float num = quat.x * 2.0; @@ -54,11 +58,12 @@ float _determinant(mat2 m) { return m[0][0] * m[1][1] - m[0][1] * m[1][0]; } -flat out uint frag_instance_id; - void main(){ - vec2 bbox_signed_radius = 0.5 * get_markersize(); // note; components may be negative. - vec2 sprite_bbox_centre = get_quad_offset() + bbox_signed_radius; + // get_pos() returns the position of the scatter marker + // get_position() returns the (relative) position of the current quad vertex + + vec2 bbox_radius = 0.5 * get_markersize(); + vec2 sprite_bbox_centre = get_quad_offset() + bbox_radius; mat4 pview = projection * view; mat4 trans = get_transform_marker() ? model : mat4(1.0); @@ -111,14 +116,25 @@ void main(){ // any calculation based on them will not be a distance function.) // * For sampled distance fields, we need to consistently choose the *x* // for the scaling in get_distancefield_scale(). - float sprite_from_u_scale = abs(get_markersize().x); + float sprite_from_u_scale = min(abs(get_markersize().x), abs(get_markersize().y)); frag_uvscale = viewport_from_sprite_scale * sprite_from_u_scale; frag_distancefield_scale = distancefield_scale(); + + // add padding for AA, stroke and glow (non native distancefields don't need + // AA padding but CIRCLE etc do) + vec2 padded_bbox_size = bbox_radius + ( + ANTIALIAS_RADIUS + max(0.0, get_strokewidth()) + max(0.0, get_glowwidth()) + ) / viewport_from_sprite_scale; + vec2 uv_pad_scale = padded_bbox_size / bbox_radius; + frag_color = tovec4(get_color()); - frag_uv = get_uv(); frag_uv_offset_width = get_uv_offset_width(); + // get_uv() returns (0, 0), (1, 0), (0, 1) or (1, 1) + // to accomodate stroke and glowwidth we need to extrude uv's outwards from (0.5, 0.5) + frag_uv = vec2(0.5) + (get_uv() - vec2(0.5)) * uv_pad_scale; + // screen space coordinates of the position - vec4 quad_vertex = (trans * vec4(2.0 * bbox_signed_radius * get_position(), 0.0, 0.0)); + vec4 quad_vertex = (trans * vec4(2.0 * padded_bbox_size * get_position(), 0.0, 0.0)); gl_Position = vclip + quad_vertex; gl_Position.z += gl_Position.w * get_depth_shift(); diff --git a/WGLMakie/src/particles.jl b/WGLMakie/src/particles.jl index 5c4a3bc681f..ae087c43a02 100644 --- a/WGLMakie/src/particles.jl +++ b/WGLMakie/src/particles.jl @@ -201,6 +201,13 @@ function scatter_shader(scene::Scene, attributes, plot) # id + picking gets filled in JS, needs to be here to emit the correct shader uniforms uniform_dict[:picking] = false uniform_dict[:object_id] = UInt32(0) + + # Make sure these exist + get!(uniform_dict, :strokewidth, 0f0) + get!(uniform_dict, :strokecolor, RGBAf(0, 0, 0, 0)) + get!(uniform_dict, :glowwidth, 0f0) + get!(uniform_dict, :glowcolor, RGBAf(0, 0, 0, 0)) + return InstancedProgram(WebGL(), lasset("sprites.vert"), lasset("sprites.frag"), instance, VertexArray(; per_instance...), uniform_dict) end diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 200d61142c4..587257ffbda 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -43,11 +43,8 @@ excludes = Set([ "fast pixel marker", "Array of Images Scatter", "Image Scatter different sizes", - "scatter with stroke", - "scatter with glow", "lines and linestyles", "Textured meshscatter", # not yet implemented - "BezierPath marker stroke", # not yet implemented ]) Makie.inline!(Makie.automatic) diff --git a/docs/explanations/backends/wglmakie.md b/docs/explanations/backends/wglmakie.md index 777115eced3..a09cfc7ad48 100644 --- a/docs/explanations/backends/wglmakie.md +++ b/docs/explanations/backends/wglmakie.md @@ -12,7 +12,6 @@ Moving more of the implementation to JavaScript is currently the goal and will g #### Missing Backend Features -* glow & stroke for scatter markers aren't implemented yet * `lines(...)` just creates unconnected linesegments and `linestyle` isn't supported #### Browser Support