Grease Pencil: Add new Corner Types

This adds three corner types: `Round` `Sharp` and `Flat`.
These control how the corner of `Grease Pencil` line strokes are rendered.

- The `Round` type draws circular arcs, and is what `Grease Pencil`
   currently supports and is default.
- The `Flat` type cuts off the tip of the corner.
- The `Sharp` type allows for sharp corners to be created.
  If the angle is sharper than `Miter Limit` then the tip will be cut like `Flat`

These three types match the main types of `line joins` present in `SVG`
files.

This data is stored in one `Point` attribute called `miter_angle`.
This stores both the `Corner Type`, the `Miter Limit` and defaults to
the `Round` type.

This PR adds:
- Rendering of the corner types.
- An operator for setting the `Corner Type` and `Miter Angle` attribute.
- Corner types to the `Outline` operator and modifier.

Part of #145380.

Pull Request: https://projects.blender.org/blender/blender/pulls/143688
This commit is contained in:
Casey Bianco-Davis
2025-09-29 18:19:05 +02:00
committed by Falk David
parent e53f0066f9
commit d4f84619ea
16 changed files with 531 additions and 71 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<T> attribute_interpolate(const VArray<T> &input, const bke::Curves
return VArray<T>::from_container(std::move(out));
};
static VArray<float> interpolate_corners(const bke::CurvesGeometry &curves)
{
const VArray<float> miter_angles = *curves.attributes().lookup_or_default<float>(
"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<float>::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<float> eval_corners(curves.evaluated_points_num(), GP_STROKE_MITER_ANGLE_ROUND);
const VArray<int8_t> types = curves.curve_types();
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const OffsetIndices<int> 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<float> 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<int> 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<float>::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<ColorGeometry4f>(
"vertex_color", bke::AttrDomain::Point, ColorGeometry4f(0.0f, 0.0f, 0.0f, 0.0f)),
curves);
const VArray<float> miter_angles = interpolate_corners(curves);
/* Assumes that if the ".selection" attribute does not exist, all points are selected. */
const VArray<float> selection_float = *attributes.lookup_or_default<float>(
@@ -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));

View File

@@ -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);

View File

@@ -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<GreasePencil *>(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<bool> changed = false;
const Vector<MutableDrawingInfo> 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<float> miter_angles =
attributes.lookup_or_add_for_write_span<float>(
"miter_angle",
bke::AttrDomain::Point,
bke::AttributeInitVArray(
VArray<float>::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);
}
/* -------------------------------------------------------------------- */

View File

@@ -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<float3> &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<float3> all_positions,
@@ -606,6 +624,7 @@ static void generate_stroke_perimeter(const Span<float3> all_positions,
const bool use_caps,
const eGPDstroke_Caps start_cap_type,
const eGPDstroke_Caps end_cap_type,
const VArray<float> miter_angles,
const float outline_offset,
Vector<float3> &r_perimeter,
Vector<int> &r_point_counts,
@@ -636,8 +655,16 @@ static void generate_stroke_perimeter(const Span<float3> 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<int> src_material_index = *src_attributes.lookup_or_default(
"material_index", bke::AttrDomain::Curve, 0);
const VArray<float> miter_angles = *src_attributes.lookup_or_default<float>(
"miter_angle", bke::AttrDomain::Point, GP_STROKE_MITER_ANGLE_ROUND);
/* Transform positions and radii. */
Array<float3> 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,

View File

@@ -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`,