diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 6542f405e85..0583ab98f79 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -5853,6 +5853,7 @@ class VIEW3D_MT_edit_greasepencil_point(Menu): layout.separator() layout.operator_menu_enum("grease_pencil.set_handle_type", property="type") + layout.operator_menu_enum("grease_pencil.set_corner_type", property="corner_type") layout.template_node_operator_asset_menu_items(catalog_path=self.bl_label) diff --git a/source/blender/draw/engines/gpencil/gpencil_shader_shared.hh b/source/blender/draw/engines/gpencil/gpencil_shader_shared.hh index 296779f0c41..ea3b88d27be 100644 --- a/source/blender/draw/engines/gpencil/gpencil_shader_shared.hh +++ b/source/blender/draw/engines/gpencil/gpencil_shader_shared.hh @@ -39,6 +39,9 @@ enum gpLightType : uint32_t { #define GP_IS_STROKE_VERTEX_BIT (1 << 30) #define GP_VERTEX_ID_SHIFT 2 +#define GP_CORNER_TYPE_ROUND_BITS 0u +#define GP_CORNER_TYPE_BEVEL_BITS 63u +#define GP_CORNER_TYPE_MITER_NUMBER 62u /* Avoid compiler funkiness with enum types not being strongly typed in C. */ #ifndef GPU_SHADER diff --git a/source/blender/draw/engines/gpencil/shaders/gpencil_frag.glsl b/source/blender/draw/engines/gpencil/shaders/gpencil_frag.glsl index e144dac7829..799ca2c9252 100644 --- a/source/blender/draw/engines/gpencil/shaders/gpencil_frag.glsl +++ b/source/blender/draw/engines/gpencil/shaders/gpencil_frag.glsl @@ -81,11 +81,15 @@ void main() frag_color.rgb *= gpencil_lighting(); - frag_color *= gpencil_stroke_round_cap_mask(gp_interp_flat.sspos.xy, - gp_interp_flat.sspos.zw, - gp_interp_flat.aspect, - gp_interp_noperspective.thickness.x, - gp_interp_noperspective.hardness); + frag_color *= gpencil_stroke_mask(gp_interp_flat.sspos.xy, + gp_interp_flat.sspos.zw, + gp_interp_flat.sspos_adj.xy, + gp_interp_flat.sspos_adj.zw, + gp_interp.uv, + gp_interp_flat.mat_flag, + gp_interp_noperspective.thickness.x, + gp_interp_noperspective.hardness, + gp_interp_noperspective.thickness.zw); /* To avoid aliasing artifacts, we reduce the opacity of small strokes. */ frag_color *= smoothstep(0.0f, 1.0f, gp_interp_noperspective.thickness.y); diff --git a/source/blender/draw/engines/gpencil/shaders/gpencil_vert.glsl b/source/blender/draw/engines/gpencil/shaders/gpencil_vert.glsl index 9070bd3d497..203f05e6e43 100644 --- a/source/blender/draw/engines/gpencil/shaders/gpencil_vert.glsl +++ b/source/blender/draw/engines/gpencil/shaders/gpencil_vert.glsl @@ -52,6 +52,7 @@ void main() vert_strength, gp_interp.uv, gp_interp_flat.sspos, + gp_interp_flat.sspos_adj, gp_interp_flat.aspect, gp_interp_noperspective.thickness, gp_interp_noperspective.hardness); diff --git a/source/blender/draw/engines/gpencil/shaders/infos/gpencil_infos.hh b/source/blender/draw/engines/gpencil/shaders/infos/gpencil_infos.hh index ca8f3310204..2628fa9fd43 100644 --- a/source/blender/draw/engines/gpencil/shaders/infos/gpencil_infos.hh +++ b/source/blender/draw/engines/gpencil/shaders/infos/gpencil_infos.hh @@ -40,11 +40,12 @@ GPU_SHADER_NAMED_INTERFACE_END(gp_interp) GPU_SHADER_NAMED_INTERFACE_INFO(gpencil_geometry_flat_iface, gp_interp_flat) FLAT(float2, aspect) FLAT(float4, sspos) +FLAT(float4, sspos_adj) FLAT(uint, mat_flag) FLAT(float, depth) GPU_SHADER_NAMED_INTERFACE_END(gp_interp_flat) GPU_SHADER_NAMED_INTERFACE_INFO(gpencil_geometry_noperspective_iface, gp_interp_noperspective) -NO_PERSPECTIVE(float2, thickness) +NO_PERSPECTIVE(float4, thickness) NO_PERSPECTIVE(float, hardness) GPU_SHADER_NAMED_INTERFACE_END(gp_interp_noperspective) diff --git a/source/blender/draw/engines/overlay/shaders/infos/overlay_edit_mode_infos.hh b/source/blender/draw/engines/overlay/shaders/infos/overlay_edit_mode_infos.hh index 770c2f3cbed..7fa8f96753e 100644 --- a/source/blender/draw/engines/overlay/shaders/infos/overlay_edit_mode_infos.hh +++ b/source/blender/draw/engines/overlay/shaders/infos/overlay_edit_mode_infos.hh @@ -773,10 +773,11 @@ OVERLAY_INFO_VARIATIONS_MODELMAT(overlay_depth_mesh_conservative, GPU_SHADER_NAMED_INTERFACE_INFO(overlay_depth_only_gpencil_flat_iface, gp_interp_flat) FLAT(float2, aspect) FLAT(float4, sspos) +FLAT(float4, sspos_adj) GPU_SHADER_NAMED_INTERFACE_END(gp_interp_flat) GPU_SHADER_NAMED_INTERFACE_INFO(overlay_depth_only_gpencil_noperspective_iface, gp_interp_noperspective) -NO_PERSPECTIVE(float2, thickness) +NO_PERSPECTIVE(float4, thickness) NO_PERSPECTIVE(float, hardness) GPU_SHADER_NAMED_INTERFACE_END(gp_interp_noperspective) diff --git a/source/blender/draw/engines/overlay/shaders/infos/overlay_outline_infos.hh b/source/blender/draw/engines/overlay/shaders/infos/overlay_outline_infos.hh index 5860b420f72..0fb21641215 100644 --- a/source/blender/draw/engines/overlay/shaders/infos/overlay_outline_infos.hh +++ b/source/blender/draw/engines/overlay/shaders/infos/overlay_outline_infos.hh @@ -90,10 +90,11 @@ OVERLAY_INFO_CLIP_VARIATION(overlay_outline_prepass_wire) GPU_SHADER_NAMED_INTERFACE_INFO(overlay_outline_prepass_gpencil_flat_iface, gp_interp_flat) FLAT(float2, aspect) FLAT(float4, sspos) +FLAT(float4, sspos_adj) GPU_SHADER_NAMED_INTERFACE_END(gp_interp_flat) GPU_SHADER_NAMED_INTERFACE_INFO(overlay_outline_prepass_gpencil_noperspective_iface, gp_interp_noperspective) -NO_PERSPECTIVE(float2, thickness) +NO_PERSPECTIVE(float4, thickness) NO_PERSPECTIVE(float, hardness) GPU_SHADER_NAMED_INTERFACE_END(gp_interp_noperspective) diff --git a/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_frag.glsl b/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_frag.glsl index 5ab9514dbb8..b49bd3b2247 100644 --- a/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_frag.glsl +++ b/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_frag.glsl @@ -20,11 +20,13 @@ float3 ray_plane_intersection(float3 ray_ori, float3 ray_dir, float4 plane) void main() { - if (gpencil_stroke_round_cap_mask(gp_interp_flat.sspos.xy, - gp_interp_flat.sspos.zw, - gp_interp_flat.aspect, - gp_interp_noperspective.thickness.x, - gp_interp_noperspective.hardness) < 0.001f) + if (gpencil_stroke_segment_mask(gp_interp_flat.sspos.xy, + gp_interp_flat.sspos.zw, + gp_interp_flat.sspos_adj.xy, + gp_interp_flat.sspos_adj.zw, + gp_interp_noperspective.thickness.x, + gp_interp_noperspective.hardness, + gp_interp_noperspective.thickness.zw) < 0.001f) { #ifndef SELECT_ENABLE /* We cannot discard the fragment in selection mode. Otherwise we would break pipeline diff --git a/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_vert.glsl b/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_vert.glsl index d40215f6718..f53ec43faa9 100644 --- a/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_vert.glsl +++ b/source/blender/draw/engines/overlay/shaders/overlay_depth_only_gpencil_vert.glsl @@ -27,6 +27,7 @@ void main() unused_strength, unused_uv, gp_interp_flat.sspos, + gp_interp_flat.sspos_adj, gp_interp_flat.aspect, gp_interp_noperspective.thickness, gp_interp_noperspective.hardness); diff --git a/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_frag.glsl b/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_frag.glsl index f175fbd02b7..ddcf2708189 100644 --- a/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_frag.glsl +++ b/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_frag.glsl @@ -19,11 +19,13 @@ float3 ray_plane_intersection(float3 ray_ori, float3 ray_dir, float4 plane) void main() { - if (gpencil_stroke_round_cap_mask(gp_interp_flat.sspos.xy, - gp_interp_flat.sspos.zw, - gp_interp_flat.aspect, - gp_interp_noperspective.thickness.x, - gp_interp_noperspective.hardness) < 0.001f) + if (gpencil_stroke_segment_mask(gp_interp_flat.sspos.xy, + gp_interp_flat.sspos.zw, + gp_interp_flat.sspos_adj.xy, + gp_interp_flat.sspos_adj.zw, + gp_interp_noperspective.thickness.x, + gp_interp_noperspective.hardness, + gp_interp_noperspective.thickness.zw) < 0.001f) { gpu_discard_fragment(); return; diff --git a/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_vert.glsl b/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_vert.glsl index d24e1206135..a19f7a4226e 100644 --- a/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_vert.glsl +++ b/source/blender/draw/engines/overlay/shaders/overlay_outline_prepass_gpencil_vert.glsl @@ -45,6 +45,7 @@ void main() unused_strength, unused_uv, gp_interp_flat.sspos, + gp_interp_flat.sspos_adj, gp_interp_flat.aspect, gp_interp_noperspective.thickness, gp_interp_noperspective.hardness); diff --git a/source/blender/draw/intern/draw_cache_impl_grease_pencil.cc b/source/blender/draw/intern/draw_cache_impl_grease_pencil.cc index 65021fe89e8..5c37bd75e47 100644 --- a/source/blender/draw/intern/draw_cache_impl_grease_pencil.cc +++ b/source/blender/draw/intern/draw_cache_impl_grease_pencil.cc @@ -223,7 +223,10 @@ static GreasePencilBatchCache *grease_pencil_batch_cache_get(GreasePencil &greas /** \name Vertex Buffers * \{ */ -BLI_INLINE int32_t pack_rotation_aspect_hardness(float rot, float asp, float softness) +BLI_INLINE int32_t pack_rotation_aspect_hardness_miter(const float rot, + const float asp, + const float softness, + const float miter_angle) { int32_t packed = 0; /* Aspect uses 9 bits */ @@ -247,6 +250,21 @@ BLI_INLINE int32_t pack_rotation_aspect_hardness(float rot, float asp, float sof } /* Hardness uses 8 bits */ packed |= int32_t(unit_float_to_uchar_clamp(1.0f - softness)) << 18; + + /* Miter Angle uses the last 6 bits */ + if (miter_angle <= GP_STROKE_MITER_ANGLE_ROUND) { + packed |= GP_CORNER_TYPE_ROUND_BITS << 26; + } + else if (miter_angle >= GP_STROKE_MITER_ANGLE_BEVEL) { + packed |= GP_CORNER_TYPE_BEVEL_BITS << 26; + } + else { + const float miter_norm = (miter_angle / M_PI); + packed |= int32_t(clamp_i( + int(miter_norm * GP_CORNER_TYPE_MITER_NUMBER), 1, GP_CORNER_TYPE_MITER_NUMBER)) + << 26; + } + return packed; } @@ -1058,6 +1076,59 @@ static VArray attribute_interpolate(const VArray &input, const bke::Curves return VArray::from_container(std::move(out)); }; +static VArray interpolate_corners(const bke::CurvesGeometry &curves) +{ + const VArray miter_angles = *curves.attributes().lookup_or_default( + "miter_angle", bke::AttrDomain::Point, GP_STROKE_MITER_ANGLE_ROUND); + + if (curves.is_single_type(CURVE_TYPE_POLY)) { + return miter_angles; + } + + if (miter_angles.is_single() && + miter_angles.get_internal_single() == GP_STROKE_MITER_ANGLE_ROUND) + { + return VArray::from_single(GP_STROKE_MITER_ANGLE_ROUND, curves.evaluated_points_num()); + } + + /* Default all the evaluated points to be round. + * This is done so that the added points look as smooth as possible. */ + Array eval_corners(curves.evaluated_points_num(), GP_STROKE_MITER_ANGLE_ROUND); + + const VArray types = curves.curve_types(); + const OffsetIndices points_by_curve = curves.points_by_curve(); + const OffsetIndices evaluated_points_by_curve = curves.evaluated_points_by_curve(); + + threading::parallel_for(curves.curves_range(), 128, [&](IndexRange range) { + for (const int curve_i : range) { + const IndexRange eval_points = evaluated_points_by_curve[curve_i]; + const IndexRange points = points_by_curve[curve_i]; + MutableSpan eval_corners_range = eval_corners.as_mutable_span().slice(eval_points); + + switch (types[curve_i]) { + case CURVE_TYPE_POLY: + for (const int i : points.index_range()) { + eval_corners_range[i] = miter_angles[points[i]]; + } + break; + case CURVE_TYPE_BEZIER: { + const Span offsets = curves.bezier_evaluated_offsets_for_curve(curve_i); + for (const int i : points.index_range()) { + eval_corners_range[offsets[i]] = miter_angles[points[i]]; + } + break; + } + case CURVE_TYPE_NURBS: + case CURVE_TYPE_CATMULL_ROM: { + /* NUBRS and Catmull-Rom are continuous and don't have corners. */ + break; + } + } + } + }); + return VArray::from_container(std::move(eval_corners)); +} + static void grease_pencil_geom_batch_ensure(Object &object, const GreasePencil &grease_pencil, const Scene &scene) @@ -1183,6 +1254,7 @@ static void grease_pencil_geom_batch_ensure(Object &object, *attributes.lookup_or_default( "vertex_color", bke::AttrDomain::Point, ColorGeometry4f(0.0f, 0.0f, 0.0f, 0.0f)), curves); + const VArray miter_angles = interpolate_corners(curves); /* Assumes that if the ".selection" attribute does not exist, all points are selected. */ const VArray selection_float = *attributes.lookup_or_default( @@ -1243,8 +1315,11 @@ static void grease_pencil_geom_batch_ensure(Object &object, * ensure the material used by the shader is valid this needs to be clamped to zero. */ s_vert.mat = std::max(materials[curve_i], 0) % GPENCIL_MATERIAL_BUFFER_LEN; - s_vert.packed_asp_hard_rot = pack_rotation_aspect_hardness( - rotations[point_i], stroke_point_aspect_ratios[curve_i], stroke_softness[curve_i]); + s_vert.packed_asp_hard_rot = pack_rotation_aspect_hardness_miter( + rotations[point_i], + stroke_point_aspect_ratios[curve_i], + stroke_softness[curve_i], + miter_angles[point_i]); s_vert.u_stroke = u_stroke; copy_v2_v2(s_vert.uv_fill, texture_matrix * float4(pos, 1.0f)); diff --git a/source/blender/draw/intern/shaders/draw_grease_pencil_lib.glsl b/source/blender/draw/intern/shaders/draw_grease_pencil_lib.glsl index 09b15560689..3b8d1f032a0 100644 --- a/source/blender/draw/intern/shaders/draw_grease_pencil_lib.glsl +++ b/source/blender/draw/intern/shaders/draw_grease_pencil_lib.glsl @@ -14,6 +14,7 @@ SHADER_LIBRARY_CREATE_INFO(draw_gpencil) #include "gpu_shader_math_constants_lib.glsl" #include "gpu_shader_math_matrix_transform_lib.glsl" +#include "gpu_shader_math_vector_lib.glsl" #include "gpu_shader_math_vector_safe_lib.glsl" #include "gpu_shader_utildefines_lib.glsl" @@ -21,37 +22,156 @@ SHADER_LIBRARY_CREATE_INFO(draw_gpencil) # error Missing additional info draw_gpencil #endif -#ifdef GPU_FRAGMENT_SHADER -float gpencil_stroke_round_cap_mask( - float2 p1, float2 p2, float2 aspect, float thickness, float hardfac) -{ - /* We create our own uv space to avoid issues with triangulation and linear - * interpolation artifacts. */ - float2 line = p2.xy - p1.xy; - float2 pos = gl_FragCoord.xy - p1.xy; - float line_len = length(line); - float half_line_len = line_len * 0.5f; - /* Normalize */ - line = (line_len > 0.0f) ? (line / line_len) : float2(1.0f, 0.0f); - /* Create a uv space that englobe the whole segment into a capsule. */ - float2 uv_end; - uv_end.x = max(abs(dot(line, pos) - half_line_len) - half_line_len, 0.0f); - uv_end.y = dot(float2(-line.y, line.x), pos); - /* Divide by stroke radius. */ - uv_end /= thickness; - uv_end *= aspect; +#define MITER_LIMIT_TYPE_BEVEL -1.0f +#define MITER_LIMIT_TYPE_ROUND -2.0f - float dist = clamp(1.0f - length(uv_end) * 2.0f, 0.0f, 1.0f); +#ifdef GPU_FRAGMENT_SHADER +float gpencil_stroke_hardess_mask(float dist, float hardfac) +{ + dist = clamp(1.0f - dist, 0.0f, 1.0f); if (hardfac > 0.999f) { return step(1e-8f, dist); } else { /* Modulate the falloff profile */ float hardness = 1.0f - hardfac; - dist = pow(dist, mix(0.01f, 10.0f, hardness)); + dist = pow(dist, mix(0.0f, 10.0f, hardness)); return smoothstep(0.0f, 1.0f, dist); } } + +/** + * + * Calculate the mask for the pixel in the main segment (1) by using the distance factor to + * the center line. + * + * *====================* + * \ / + * p1------1>------p2 + * / \ / \ + * / *============* \ + * 0 1 + * / \ + * / \ + * p0 p3 + * + * Segments: 0 (p0->p1) + * 1 (p1->p2) + * 2 (p2->p3) + * + * Each point can have a different corner type, stored as p1: miter_limit.x, p2: miter_limit.y + * + */ +float gpencil_stroke_segment_mask( + float2 p1, float2 p2, float2 p0, float2 p3, float thickness, float hardfac, float2 miter_limit) +{ + bool both_round = miter_limit.x == MITER_LIMIT_TYPE_ROUND && + miter_limit.y == MITER_LIMIT_TYPE_ROUND; + + bool is_start = distance_squared(p0, p1) < 1e-6; + bool is_end = distance_squared(p2, p3) < 1e-6; + bool both_ends = is_start && is_end; + + float radius = thickness * 0.5f; + float2 pos1 = gl_FragCoord.xy - p1; + float2 line1 = p2 - p1; + float2 tan1 = orthogonal(line1); + float len_sq1 = length_squared(line1); + + /* Calculate the factor along the main segment. */ + float t1 = dot(pos1, line1) / len_sq1; + + /* The distance factor squared to the main segment. This is clamped and will lead to round + * corners. */ + float dist = length_squared(pos1 - saturate(t1) * line1); + + if (both_round || both_ends) { + dist = sqrt(dist) / radius; + return gpencil_stroke_hardess_mask(dist, hardfac); + } + + float2 line0 = p1 - p0; + float2 tan0 = orthogonal(line0); + float len_sq0 = length_squared(line0); + + float2 pos2 = gl_FragCoord.xy - p2; + float2 line2 = p3 - p2; + float2 tan2 = orthogonal(line2); + float len_sq2 = length_squared(line2); + + /* Calculate the non-normalized factor along the other segments. */ + float t0 = dot(pos1, line0); + float t2 = dot(pos2, line2); + + /* Normalize all segment directions. */ + float2 tan_norm0 = tan0 / sqrt(len_sq0); + float2 tan_norm1 = tan1 / sqrt(len_sq1); + float2 tan_norm2 = tan2 / sqrt(len_sq2); + + /* Get the squared distance to the main segment. */ + float dist_sq_1 = length_squared(pos1 - t1 * line1); + + /* Check if the pixel is within the corner region between segments 1 and 0. */ + if (t1 <= 0.0f && t0 >= 0.0f && !is_start && miter_limit.x != MITER_LIMIT_TYPE_ROUND) { + if (miter_limit.x == MITER_LIMIT_TYPE_BEVEL) { + /* Bevel by cutting with a the half angle line. */ + float2 bevel_tan = orthogonal(tan_norm0 - tan_norm1); + + dist = dot(pos1, bevel_tan) / dot(tan_norm1, bevel_tan); + dist *= dist; + } + else { + /* Continue the main line to get a sharp corner. */ + dist = dist_sq_1; + } + } + + /* Check if the pixel is within the corner region between segments 1 and 2. */ + if (t1 >= 1.0f && t2 <= 0.0f && !is_end && miter_limit.y != MITER_LIMIT_TYPE_ROUND) { + if (miter_limit.y == MITER_LIMIT_TYPE_BEVEL) { + /* Bevel by cutting with a the half angle line. */ + float2 bevel_tan = orthogonal(tan_norm2 - tan_norm1); + + dist = dot(pos2, bevel_tan) / dot(tan_norm1, bevel_tan); + dist *= dist; + } + else { + /* Continue the main line to get a sharp corner. */ + dist = dist_sq_1; + } + } + + dist = sqrt(dist) / radius; + return gpencil_stroke_hardess_mask(dist, hardfac); +} + +float gpencil_stroke_mask(float2 p1, + float2 p2, + float2 p0, + float2 p3, + float2 uv, + uint mat_flag, + float thickness, + float hardfac, + float2 miter_limit) +{ + if (flag_test(mat_flag, GP_STROKE_ALIGNMENT)) { + /* Dot or Squares. */ + uv = uv * 2.0 - 1.0; + if (flag_test(mat_flag, GP_STROKE_DOTS)) { + return gpencil_stroke_hardess_mask(length(uv), hardfac); + } + else { + uv = abs(uv); + return gpencil_stroke_hardess_mask(max(uv.x, uv.y), hardfac); + } + } + else { + /* Line mask */ + return gpencil_stroke_segment_mask(p1, p2, p0, p3, thickness, hardfac, miter_limit); + } +} + #endif struct PointData { @@ -93,6 +213,19 @@ float gpencil_decode_hardness(int packed_data) return float((uint(packed_data) & 0x3FC0000u) >> 18u) * (1.0f / 255.0f); } +float gpencil_decode_miter_limit(int packed_data) +{ + uint miter_data = (uint(packed_data) & 0xFC000000u) >> 26u; + if (miter_data == GP_CORNER_TYPE_ROUND_BITS) { + return MITER_LIMIT_TYPE_ROUND; + } + else if (miter_data == GP_CORNER_TYPE_BEVEL_BITS) { + return MITER_LIMIT_TYPE_BEVEL; + } + float miter_angle = float(miter_data) * (M_PI / GP_CORNER_TYPE_MITER_NUMBER); + return cos(miter_angle); +} + float2 gpencil_project_to_screenspace(float4 v, float4 viewport_res) { return ((v.xy / v.w) * 0.5f + 0.5f) * viewport_res.xy; @@ -161,14 +294,17 @@ float4 gpencil_vertex(float4 viewport_res, out float2 out_uv, /* Screen-Space segment endpoints. */ out float4 out_sspos, + /* Screen-Space adjacent segment endpoints. */ + out float4 out_sspos_adj, /* Stroke aspect ratio. */ out float2 out_aspect, - /* Stroke thickness (x: clamped, y: unclamped). */ - out float2 out_thickness, + /* Stroke thickness and miter limits (x: clamped, y: unclamped, + * z: miter limit segment start, w: miter limit segment end). */ + out float4 out_thickness, /* Stroke hardness. */ out float out_hardness) { - int stroke_point_id = (gl_VertexID & ~GP_IS_STROKE_VERTEX_BIT) >> GP_VERTEX_ID_SHIFT; + int stroke_point_id = gpencil_stroke_point_id(); /* Attribute Loading. */ float4 pos = texelFetch(gp_pos_tx, (stroke_point_id - 1) * 3 + 0); @@ -240,9 +376,11 @@ float4 gpencil_vertex(float4 viewport_res, bool use_curr = is_dot || (x == -1.0f); - float3 wpos_adj = transform_point(drw_modelmat(), (use_curr) ? pos.xyz : pos3.xyz); + float3 wpos0 = transform_point(drw_modelmat(), pos.xyz); float3 wpos1 = transform_point(drw_modelmat(), pos1.xyz); float3 wpos2 = transform_point(drw_modelmat(), pos2.xyz); + float3 wpos3 = transform_point(drw_modelmat(), pos3.xyz); + float3 wpos_adj = (use_curr) ? wpos0 : wpos3; float3 T; if (is_dot) { @@ -260,21 +398,26 @@ float4 gpencil_vertex(float4 viewport_res, float3 B = cross(T, drw_view().viewinv[2].xyz); out_N = normalize(cross(B, T)); - float4 ndc_adj = drw_point_world_to_homogenous(wpos_adj); + float4 ndc0 = drw_point_world_to_homogenous(wpos0); float4 ndc1 = drw_point_world_to_homogenous(wpos1); float4 ndc2 = drw_point_world_to_homogenous(wpos2); + float4 ndc3 = drw_point_world_to_homogenous(wpos3); out_ndc = (use_curr) ? ndc1 : ndc2; out_P = (use_curr) ? wpos1 : wpos2; out_strength = abs((use_curr) ? strength1 : strength2); - float2 ss_adj = gpencil_project_to_screenspace(ndc_adj, viewport_res); + float2 ss0 = gpencil_project_to_screenspace(ndc0, viewport_res); float2 ss1 = gpencil_project_to_screenspace(ndc1, viewport_res); float2 ss2 = gpencil_project_to_screenspace(ndc2, viewport_res); + float2 ss3 = gpencil_project_to_screenspace(ndc3, viewport_res); + /* Screen-space Lines tangents. */ float line_len; float2 line = safe_normalize_and_get_length(ss2 - ss1, line_len); - float2 line_adj = safe_normalize((use_curr) ? (ss1 - ss_adj) : (ss_adj - ss2)); + float2 line1 = safe_normalize(ss1 - ss0); + float2 line2 = safe_normalize(ss3 - ss2); + float2 line_adj = (use_curr) ? line1 : line2; float thickness = abs((use_curr) ? thickness1 : thickness2); thickness = gpencil_stroke_thickness_modulate(thickness, out_ndc, viewport_res); @@ -286,6 +429,26 @@ float4 gpencil_vertex(float4 viewport_res, out_hardness = gpencil_decode_hardness(use_curr ? point_data1.packed_data : point_data2.packed_data); + out_sspos.xy = ss1; + if (ma2.x != -1) { + out_sspos.zw = ss2; + } + else { + out_sspos.zw = out_sspos.xy; + } + if (ma.x != -1) { + out_sspos_adj.xy = ss0; + } + else { + out_sspos_adj.xy = out_sspos.xy; + } + if (ma3.x != -1) { + out_sspos_adj.zw = ss3; + } + else { + out_sspos_adj.zw = out_sspos.zw; + } + if (is_dot) { uint alignment_mode = material_flags & GP_STROKE_ALIGNMENT; @@ -330,28 +493,53 @@ float4 gpencil_vertex(float4 viewport_res, out_ndc.xy += (x * x_axis + y * y_axis) * viewport_res.zw * clamped_thickness; - out_sspos.xy = ss1; - out_sspos.zw = ss1 + x_axis * 0.5f; out_thickness.x = (is_squares) ? 1e18f : (clamped_thickness / out_ndc.w); out_thickness.y = (is_squares) ? 1e18f : (thickness / out_ndc.w); + out_thickness.z = MITER_LIMIT_TYPE_ROUND; + out_thickness.w = MITER_LIMIT_TYPE_ROUND; } else { bool is_stroke_start = (ma.x == -1 && x == -1); bool is_stroke_end = (ma3.x == -1 && x == 1); + float miter_limit1 = gpencil_decode_miter_limit(point_data1.packed_data); + float miter_limit2 = gpencil_decode_miter_limit(point_data2.packed_data); + + float cos_angle1 = -dot(line, line1); + float cos_angle2 = -dot(line, line2); + + out_thickness.z = miter_limit1; + out_thickness.w = miter_limit2; + + if (cos_angle1 > miter_limit1 && miter_limit1 != MITER_LIMIT_TYPE_ROUND) { + out_thickness.z = MITER_LIMIT_TYPE_BEVEL; + } + if (cos_angle2 > miter_limit2 && miter_limit2 != MITER_LIMIT_TYPE_ROUND) { + out_thickness.w = MITER_LIMIT_TYPE_BEVEL; + } + + float miter_limit = use_curr ? miter_limit1 : miter_limit2; + + if (miter_limit == MITER_LIMIT_TYPE_BEVEL || miter_limit == MITER_LIMIT_TYPE_ROUND) { + miter_limit = 0.5f; /* Default to cos(60) */ + } + + /* Prevent the limit from becoming to small. */ + if (miter_limit < 0.5f) { + miter_limit = 0.5f; + } + /* Mitter tangent vector. */ float2 miter_tan = safe_normalize(line_adj + line); float miter_dot = dot(miter_tan, line_adj); + float cos_angle_adj = (use_curr) ? cos_angle1 : cos_angle2; /* Break corners after a certain angle to avoid really thick corners. */ - const float miter_limit = 0.5f; /* cos(60 degrees) */ - bool miter_break = (miter_dot < miter_limit); + bool miter_break = cos_angle_adj > miter_limit; miter_tan = (miter_break || is_stroke_start || is_stroke_end) ? line : (miter_tan / miter_dot); /* Rotate 90 degrees counter-clockwise. */ float2 miter = float2(-miter_tan.y, miter_tan.x); - out_sspos.xy = ss1; - out_sspos.zw = ss2; out_thickness.x = clamped_thickness / out_ndc.w; out_thickness.y = thickness / out_ndc.w; out_aspect = float2(1.0f); @@ -378,9 +566,12 @@ float4 gpencil_vertex(float4 viewport_res, out_uv = uv1.xy; out_thickness.x = 1e18f; out_thickness.y = 1e20f; + out_thickness.z = MITER_LIMIT_TYPE_ROUND; + out_thickness.w = MITER_LIMIT_TYPE_ROUND; out_hardness = 1.0f; out_aspect = float2(1.0f); out_sspos = float4(0.0f); + out_sspos_adj = float4(0.0f); /* Flat normal following camera and object bounds. */ float3 V = drw_world_incident_vector(drw_modelmat()[3].xyz); @@ -411,8 +602,9 @@ float4 gpencil_vertex(float4 viewport_res, out float out_strength, out float2 out_uv, out float4 out_sspos, + out float4 out_sspos_adj, out float2 out_aspect, - out float2 out_thickness, + out float4 out_thickness, out float out_hardness) { return gpencil_vertex(viewport_res, @@ -424,6 +616,7 @@ float4 gpencil_vertex(float4 viewport_res, out_strength, out_uv, out_sspos, + out_sspos_adj, out_aspect, out_thickness, out_hardness); diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc index 1b23a7a5f87..66acafabc39 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_edit.cc @@ -4722,6 +4722,146 @@ static void GREASE_PENCIL_OT_convert_curve_type(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Set Corner Type Operator + * \{ */ + +enum class CornerType : uint8_t { + Round = 0, + Bevel = 1, + Miter = 2, +}; + +static const EnumPropertyItem prop_corner_types[] = { + {int(CornerType::Round), "ROUND", 0, "Round", ""}, + {int(CornerType::Bevel), "FLAT", 0, "Flat", ""}, + {int(CornerType::Miter), "SHARP", 0, "Sharp", ""}, + {0, nullptr, 0, nullptr, nullptr}, +}; + +static wmOperatorStatus grease_pencil_set_corner_type_exec(bContext *C, wmOperator *op) +{ + using bke::greasepencil::Layer; + + const Scene *scene = CTX_data_scene(C); + Object *object = CTX_data_active_object(C); + View3D *v3d = CTX_wm_view3d(C); + GreasePencil &grease_pencil = *static_cast(object->data); + + const CornerType corner_type = CornerType(RNA_enum_get(op->ptr, "corner_type")); + float miter_angle = RNA_float_get(op->ptr, "miter_angle"); + + if (corner_type == CornerType::Round) { + miter_angle = GP_STROKE_MITER_ANGLE_ROUND; + } + else if (corner_type == CornerType::Bevel) { + miter_angle = GP_STROKE_MITER_ANGLE_BEVEL; + } + else if (corner_type == CornerType::Miter) { + /* Prevent the angle from being set to zero, and becoming the `Round` type.*/ + if (miter_angle == 0.0f) { + miter_angle = DEG2RADF(1.0f); + } + } + + std::atomic changed = false; + const Vector drawings = retrieve_editable_drawings(*scene, grease_pencil); + threading::parallel_for_each(drawings, [&](const MutableDrawingInfo &info) { + IndexMaskMemory memory; + const IndexMask selection = ed::greasepencil::retrieve_editable_and_all_selected_points( + *object, info.drawing, info.layer_index, v3d->overlay.handle_display, memory); + if (selection.is_empty()) { + return; + } + + bke::CurvesGeometry &curves = info.drawing.strokes_for_write(); + bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); + + /* Only create the attribute if we are not storing the default. */ + if (miter_angle == GP_STROKE_MITER_ANGLE_ROUND && !attributes.contains("miter_angle")) { + return; + } + + /* Remove the attribute if we are storing all default. */ + if (miter_angle == GP_STROKE_MITER_ANGLE_ROUND && selection == curves.points_range()) { + attributes.remove("miter_angle"); + changed.store(true, std::memory_order_relaxed); + return; + } + + if (bke::SpanAttributeWriter miter_angles = + attributes.lookup_or_add_for_write_span( + "miter_angle", + bke::AttrDomain::Point, + bke::AttributeInitVArray( + VArray::from_single(GP_STROKE_MITER_ANGLE_ROUND, curves.points_num())))) + { + index_mask::masked_fill(miter_angles.span, miter_angle, selection); + miter_angles.finish(); + changed.store(true, std::memory_order_relaxed); + } + }); + + if (changed) { + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil); + } + + return OPERATOR_FINISHED; +} + +static void grease_pencil_set_corner_type_ui(bContext *C, wmOperator *op) +{ + uiLayout *layout = op->layout; + wmWindowManager *wm = CTX_wm_manager(C); + + PointerRNA ptr = RNA_pointer_create_discrete(&wm->id, op->type->srna, op->properties); + + layout->use_property_split_set(true); + layout->use_property_decorate_set(false); + + layout->prop(&ptr, "corner_type", UI_ITEM_NONE, std::nullopt, ICON_NONE); + + const CornerType corner_type = CornerType(RNA_enum_get(op->ptr, "corner_type")); + + if (corner_type != CornerType::Miter) { + return; + } + + layout->prop(&ptr, "miter_angle", UI_ITEM_NONE, std::nullopt, ICON_NONE); +} + +static void GREASE_PENCIL_OT_set_corner_type(wmOperatorType *ot) +{ + /* Identifiers. */ + ot->name = "Set Corner Type"; + ot->idname = "GREASE_PENCIL_OT_set_corner_type"; + ot->description = "Set the corner type of the selected points"; + + /* Callbacks. */ + ot->exec = grease_pencil_set_corner_type_exec; + ot->poll = editable_grease_pencil_poll; + ot->ui = grease_pencil_set_corner_type_ui; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* Properties */ + ot->prop = RNA_def_enum( + ot->srna, "corner_type", prop_corner_types, int(CornerType::Miter), "Corner Type", ""); + ot->prop = RNA_def_float_distance(ot->srna, + "miter_angle", + DEG2RADF(45.0f), + 0.0f, + M_PI, + "Miter Cut Angle", + "All corners sharper than the Miter angle will be cut flat", + 0.0f, + M_PI); + RNA_def_property_subtype(ot->prop, PROP_ANGLE); +} + +/** \} */ + } // namespace blender::ed::greasepencil void ED_operatortypes_grease_pencil_edit() @@ -4765,6 +4905,7 @@ void ED_operatortypes_grease_pencil_edit() WM_operatortype_append(GREASE_PENCIL_OT_remove_fill_guides); WM_operatortype_append(GREASE_PENCIL_OT_outline); WM_operatortype_append(GREASE_PENCIL_OT_convert_curve_type); + WM_operatortype_append(GREASE_PENCIL_OT_set_corner_type); } /* -------------------------------------------------------------------- */ diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_geom.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_geom.cc index 8d3493b38fd..75185a4d2e5 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_geom.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_geom.cc @@ -550,13 +550,14 @@ static void generate_cap(const float3 &point, } } -/* Generate a corner between two segments, with a rounded outer perimeter. +/* Generate a corner between two segments, using `miter_limit_angle` as the corner type. * NOTE: The perimeter is considered to be to the right hand side of the stroke. The left side * perimeter can be generated by reversing the order of points. */ static void generate_corner(const float3 &pt_a, const float3 &pt_b, const float3 &pt_c, const float radius, + const float miter_limit_angle, const int corner_subdivisions, const int src_point_index, Vector &r_perimeter, @@ -573,7 +574,7 @@ static void generate_corner(const float3 &pt_a, /* Whether the corner is an inside or outside corner. * This determines whether an arc is added or a single miter point. */ const bool is_outside_corner = (sin_angle >= 0.0f); - if (is_outside_corner) { + if (is_outside_corner && miter_limit_angle <= GP_STROKE_MITER_ANGLE_ROUND) { generate_arc_from_point_to_point(pt_b + normal_prev * radius, pt_b + normal * radius, pt_b, @@ -581,21 +582,38 @@ static void generate_corner(const float3 &pt_a, src_point_index, r_perimeter, r_src_indices); + return; } - else { - const float2 avg_tangent = math::normalize(tangent_prev + tangent); - const float3 miter = {avg_tangent.y, -avg_tangent.x, 0.0f}; - const float miter_invscale = math::dot(normal, miter); - /* Avoid division by tiny values for steep angles. */ - const float3 miter_point = (radius < length * miter_invscale && - radius < length_prev * miter_invscale) ? - pt_b + miter * radius / miter_invscale : - pt_b + miter * radius; + const float2 avg_tangent = math::normalize(tangent_prev + tangent); + const float3 miter = {avg_tangent.y, -avg_tangent.x, 0.0f}; + const float miter_invscale = math::dot(normal, miter); - r_perimeter.append(miter_point); - r_src_indices.append(src_point_index); + if (is_outside_corner) { + const bool is_bevel = -math::dot(tangent, tangent_prev) > math::cos(miter_limit_angle); + if (is_bevel) { + r_perimeter.append(pt_b + normal_prev * radius); + r_perimeter.append(pt_b + normal * radius); + r_src_indices.append_n_times(src_point_index, 2); + return; + } + else { + const float3 miter_point = pt_b + miter * radius / miter_invscale; + + r_perimeter.append(miter_point); + r_src_indices.append(src_point_index); + return; + } } + + /* Avoid division by tiny values for steep angles. */ + const float3 miter_point = (radius < length * miter_invscale && + radius < length_prev * miter_invscale) ? + pt_b + miter * radius / miter_invscale : + pt_b + miter * radius; + + r_perimeter.append(miter_point); + r_src_indices.append(src_point_index); } static void generate_stroke_perimeter(const Span all_positions, @@ -606,6 +624,7 @@ static void generate_stroke_perimeter(const Span all_positions, const bool use_caps, const eGPDstroke_Caps start_cap_type, const eGPDstroke_Caps end_cap_type, + const VArray miter_angles, const float outline_offset, Vector &r_perimeter, Vector &r_point_counts, @@ -636,8 +655,16 @@ static void generate_stroke_perimeter(const Span all_positions, const float3 pt_b = positions[b]; const float3 pt_c = positions[c]; const float radius = std::max(all_radii[point] + outline_offset, 0.0f); - generate_corner( - pt_a, pt_b, pt_c, radius, corner_subdivisions, point, r_perimeter, r_point_indices); + const float miter_angle = miter_angles[point]; + generate_corner(pt_a, + pt_b, + pt_c, + radius, + miter_angle, + corner_subdivisions, + point, + r_perimeter, + r_point_indices); }; auto add_cap = [&](const int center_i, const int next_i, const eGPDstroke_Caps cap_type) { const int point = points[center_i]; @@ -748,6 +775,8 @@ bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &draw "end_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_ROUND); const VArray src_material_index = *src_attributes.lookup_or_default( "material_index", bke::AttrDomain::Curve, 0); + const VArray miter_angles = *src_attributes.lookup_or_default( + "miter_angle", bke::AttrDomain::Point, GP_STROKE_MITER_ANGLE_ROUND); /* Transform positions and radii. */ Array transformed_positions(src_positions.size()); @@ -784,6 +813,7 @@ bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &draw use_caps, eGPDstroke_Caps(src_start_caps[curve_i]), eGPDstroke_Caps(src_end_caps[curve_i]), + miter_angles, outline_offset, data.positions, data.point_counts, diff --git a/source/blender/makesdna/DNA_grease_pencil_types.h b/source/blender/makesdna/DNA_grease_pencil_types.h index 4884feebd4d..b23f5bc7fc3 100644 --- a/source/blender/makesdna/DNA_grease_pencil_types.h +++ b/source/blender/makesdna/DNA_grease_pencil_types.h @@ -60,6 +60,9 @@ typedef enum GreasePencilStrokeCapType { GP_STROKE_CAP_TYPE_MAX, } GreasePencilStrokeCapType; +#define GP_STROKE_MITER_ANGLE_ROUND 0.0f +#define GP_STROKE_MITER_ANGLE_BEVEL DEG2RADF(180.0f) + /** * Type of drawing data. * If `GP_DRAWING` the node is a `GreasePencilDrawing`,