Cleanup: GPv3: Move create_curves_outline into grease_pencil_geom.cc

Moves the `create_curves_outline` into the editor file `grease_pencil_geom.cc`
in preperation to use it in other places (draw tool).

Pull Request: https://projects.blender.org/blender/blender/pulls/123383
This commit is contained in:
Falk David
2024-06-18 18:12:45 +02:00
committed by Falk David
parent 3013630137
commit 6ad04beff8
3 changed files with 412 additions and 387 deletions

View File

@@ -8,6 +8,7 @@
#include <limits>
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_kdtree.h"
#include "BLI_math_vector.hh"
#include "BLI_stack.hh"
@@ -330,4 +331,387 @@ blender::bke::CurvesGeometry curves_merge_by_distance(
return dst_curves;
}
/* Generate points in an counter-clockwise arc between two directions. */
static void generate_arc_from_point_to_point(const float3 &from,
const float3 &to,
const float3 &center_pt,
const int corner_subdivisions,
const int src_point_index,
Vector<float3> &r_perimeter,
Vector<int> &r_src_indices)
{
const float3 vec_from = from - center_pt;
const float3 vec_to = to - center_pt;
if (math::is_zero(vec_from) || math::is_zero(vec_to)) {
r_perimeter.append(center_pt);
r_src_indices.append(src_point_index);
return;
}
const float cos_angle = math::dot(vec_from.xy(), vec_to.xy());
const float sin_angle = vec_from.x * vec_to.y - vec_from.y * vec_to.x;
/* Compute angle in range [0, 2pi) so that the rotation is always counter-clockwise. */
const float angle = math::atan2(-sin_angle, -cos_angle) + M_PI;
/* Number of points is 2^(n+1) + 1 on half a circle (n=corner_subdivisions)
* so we multiply by (angle / pi) to get the right amount of
* points to insert. */
const int num_full = (1 << (corner_subdivisions + 1)) + 1;
const int num_points = num_full * math::abs(angle) / M_PI;
if (num_points < 2) {
r_perimeter.append(center_pt + vec_from);
r_src_indices.append(src_point_index);
return;
}
const float delta_angle = angle / float(num_points - 1);
const float delta_cos = math::cos(delta_angle);
const float delta_sin = math::sin(delta_angle);
float3 vec = vec_from;
for ([[maybe_unused]] const int i : IndexRange(num_points)) {
r_perimeter.append(center_pt + vec);
r_src_indices.append(src_point_index);
const float x = delta_cos * vec.x - delta_sin * vec.y;
const float y = delta_sin * vec.x + delta_cos * vec.y;
vec = float3(x, y, 0.0f);
}
}
/* Generate a semi-circle around a point, opposite the direction. */
static void generate_cap(const float3 &point,
const float3 &tangent,
const float radius,
const int corner_subdivisions,
const eGPDstroke_Caps cap_type,
const int src_point_index,
Vector<float3> &r_perimeter,
Vector<int> &r_src_indices)
{
const float3 normal = {tangent.y, -tangent.x, 0.0f};
switch (cap_type) {
case GP_STROKE_CAP_ROUND:
generate_arc_from_point_to_point(point - normal * radius,
point + normal * radius,
point,
corner_subdivisions,
src_point_index,
r_perimeter,
r_src_indices);
break;
case GP_STROKE_CAP_FLAT:
r_perimeter.append(point + normal * radius);
r_src_indices.append(src_point_index);
break;
case GP_STROKE_CAP_MAX:
BLI_assert_unreachable();
break;
}
}
/* Generate a corner between two segments, with a rounded outer perimeter.
* 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 int corner_subdivisions,
const int src_point_index,
Vector<float3> &r_perimeter,
Vector<int> &r_src_indices)
{
const float length = math::length(pt_c - pt_b);
const float length_prev = math::length(pt_b - pt_a);
const float2 tangent = math::normalize((pt_c - pt_b).xy());
const float2 tangent_prev = math::normalize((pt_b - pt_a).xy());
const float3 normal = {tangent.y, -tangent.x, 0.0f};
const float3 normal_prev = {tangent_prev.y, -tangent_prev.x, 0.0f};
const float sin_angle = tangent_prev.x * tangent.y - tangent_prev.y * tangent.x;
/* 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) {
generate_arc_from_point_to_point(pt_b + normal_prev * radius,
pt_b + normal * radius,
pt_b,
corner_subdivisions,
src_point_index,
r_perimeter,
r_src_indices);
}
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;
r_perimeter.append(miter_point);
r_src_indices.append(src_point_index);
}
}
static void generate_stroke_perimeter(const Span<float3> all_positions,
const VArray<float> all_radii,
const IndexRange points,
const int corner_subdivisions,
const bool is_cyclic,
const bool use_caps,
const eGPDstroke_Caps start_cap_type,
const eGPDstroke_Caps end_cap_type,
const float outline_offset,
Vector<float3> &r_perimeter,
Vector<int> &r_point_counts,
Vector<int> &r_point_indices)
{
const Span<float3> positions = all_positions.slice(points);
const int point_num = points.size();
if (point_num < 2) {
return;
}
auto add_corner = [&](const int a, const int b, const int c) {
const int point = points[b];
const float3 pt_a = positions[a];
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);
};
auto add_cap = [&](const int center_i, const int next_i, const eGPDstroke_Caps cap_type) {
const int point = points[center_i];
const float3 &center = positions[center_i];
const float3 dir = math::normalize(positions[next_i] - center);
const float radius = std::max(all_radii[point] + outline_offset, 0.0f);
generate_cap(
center, dir, radius, corner_subdivisions, cap_type, point, r_perimeter, r_point_indices);
};
/* Creates a single cyclic curve with end caps. */
if (use_caps) {
/* Open curves generate a start and end cap and a connecting stroke on either side. */
const int perimeter_start = r_perimeter.size();
/* Start cap. */
add_cap(0, 1, start_cap_type);
/* Right perimeter half. */
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(i - 1, i, i + 1);
}
if (is_cyclic) {
add_corner(point_num - 2, point_num - 1, 0);
}
/* End cap. */
if (is_cyclic) {
/* End point is same as start point. */
add_cap(0, point_num - 1, end_cap_type);
}
else {
/* End point is last point of the curve. */
add_cap(point_num - 1, point_num - 2, end_cap_type);
}
/* Left perimeter half. */
if (is_cyclic) {
add_corner(0, point_num - 1, point_num - 2);
}
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(point_num - i, point_num - i - 1, point_num - i - 2);
}
const int perimeter_count = r_perimeter.size() - perimeter_start;
if (perimeter_count > 0) {
r_point_counts.append(perimeter_count);
}
}
else {
/* Generate separate "inside" and an "outside" perimeter curves.
* The distinction is arbitrary, called left/right here. */
/* Right side perimeter. */
const int left_perimeter_start = r_perimeter.size();
add_corner(point_num - 1, 0, 1);
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(i - 1, i, i + 1);
}
add_corner(point_num - 2, point_num - 1, 0);
const int left_perimeter_count = r_perimeter.size() - left_perimeter_start;
if (left_perimeter_count > 0) {
r_point_counts.append(left_perimeter_count);
}
/* Left side perimeter. */
const int right_perimeter_start = r_perimeter.size();
add_corner(0, point_num - 1, point_num - 2);
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(point_num - i, point_num - i - 1, point_num - i - 2);
}
add_corner(1, 0, point_num - 1);
const int right_perimeter_count = r_perimeter.size() - right_perimeter_start;
if (right_perimeter_count > 0) {
r_point_counts.append(right_perimeter_count);
}
}
}
struct PerimeterData {
/* New points per curve count. */
Vector<int> point_counts;
/* New point coordinates. */
Vector<float3> positions;
/* Source curve index. */
Vector<int> curve_indices;
/* Source point index. */
Vector<int> point_indices;
};
bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &drawing,
const IndexMask &strokes,
const float4x4 &transform,
const int corner_subdivisions,
const float outline_radius,
const float outline_offset,
const int material_index)
{
const bke::CurvesGeometry &src_curves = drawing.strokes();
Span<float3> src_positions = src_curves.positions();
bke::AttributeAccessor src_attributes = src_curves.attributes();
const VArray<float> src_radii = drawing.radii();
const VArray<bool> src_cyclic = *src_attributes.lookup_or_default(
"cyclic", bke::AttrDomain::Curve, false);
const VArray<int8_t> src_start_caps = *src_attributes.lookup_or_default<int8_t>(
"start_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_ROUND);
const VArray<int8_t> src_end_caps = *src_attributes.lookup_or_default<int8_t>(
"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, -1);
/* Transform positions. */
Array<float3> transformed_positions(src_positions.size());
threading::parallel_for(transformed_positions.index_range(), 4096, [&](const IndexRange range) {
for (const int i : range) {
transformed_positions[i] = math::transform_point(transform, src_positions[i]);
}
});
const float4x4 transform_inv = math::invert(transform);
threading::EnumerableThreadSpecific<PerimeterData> thread_data;
strokes.foreach_index(GrainSize(256), [&](const int64_t curve_i) {
PerimeterData &data = thread_data.local();
const bool is_cyclic_curve = src_cyclic[curve_i];
/* NOTE: Cyclic curves would better be represented by a cyclic perimeter without end caps, but
* we always generate caps for compatibility with GPv2. Fill materials cannot create holes, so
* a cyclic outline does not work well. */
const bool use_caps = true /*!is_cyclic_curve*/;
const int prev_point_num = data.positions.size();
const int prev_curve_num = data.point_counts.size();
const IndexRange points = src_curves.points_by_curve()[curve_i];
generate_stroke_perimeter(transformed_positions,
src_radii,
points,
corner_subdivisions,
is_cyclic_curve,
use_caps,
eGPDstroke_Caps(src_start_caps[curve_i]),
eGPDstroke_Caps(src_end_caps[curve_i]),
outline_offset,
data.positions,
data.point_counts,
data.point_indices);
/* Transform perimeter positions back into object space. */
for (float3 &pos : data.positions.as_mutable_span().drop_front(prev_point_num)) {
pos = math::transform_point(transform_inv, pos);
}
data.curve_indices.append_n_times(curve_i, data.point_counts.size() - prev_curve_num);
});
int dst_curve_num = 0;
int dst_point_num = 0;
for (const PerimeterData &data : thread_data) {
BLI_assert(data.point_counts.size() == data.curve_indices.size());
BLI_assert(data.positions.size() == data.point_indices.size());
dst_curve_num += data.point_counts.size();
dst_point_num += data.positions.size();
}
bke::CurvesGeometry dst_curves(dst_point_num, dst_curve_num);
if (dst_point_num == 0 || dst_curve_num == 0) {
return dst_curves;
}
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
bke::SpanAttributeWriter<bool> dst_cyclic = dst_attributes.lookup_or_add_for_write_span<bool>(
"cyclic", bke::AttrDomain::Curve);
bke::SpanAttributeWriter<int> dst_material = dst_attributes.lookup_or_add_for_write_span<int>(
"material_index", bke::AttrDomain::Curve);
bke::SpanAttributeWriter<float> dst_radius = dst_attributes.lookup_or_add_for_write_span<float>(
"radius", bke::AttrDomain::Point);
const MutableSpan<int> dst_offsets = dst_curves.offsets_for_write();
const MutableSpan<float3> dst_positions = dst_curves.positions_for_write();
/* Source indices for attribute mapping. */
Array<int> dst_curve_map(dst_curve_num);
Array<int> dst_point_map(dst_point_num);
IndexRange curves;
IndexRange points;
for (const PerimeterData &data : thread_data) {
curves = curves.after(data.point_counts.size());
points = points.after(data.positions.size());
/* Append curve data. */
dst_curve_map.as_mutable_span().slice(curves).copy_from(data.curve_indices);
/* Curve offsets are accumulated below. */
dst_offsets.slice(curves).copy_from(data.point_counts);
dst_cyclic.span.slice(curves).fill(true);
if (material_index >= 0) {
dst_material.span.slice(curves).fill(material_index);
}
else {
for (const int i : curves.index_range()) {
dst_material.span[curves[i]] = src_material_index[data.curve_indices[i]];
}
}
/* Append point data. */
dst_positions.slice(points).copy_from(data.positions);
dst_point_map.as_mutable_span().slice(points).copy_from(data.point_indices);
dst_radius.span.slice(points).fill(outline_radius);
}
offset_indices::accumulate_counts_to_offsets(dst_curves.offsets_for_write());
bke::gather_attributes(src_attributes,
bke::AttrDomain::Point,
{},
{"position", "radius"},
dst_point_map,
dst_attributes);
bke::gather_attributes(src_attributes,
bke::AttrDomain::Curve,
{},
{"cyclic", "material_index"},
dst_curve_map,
dst_attributes);
dst_cyclic.finish();
dst_material.finish();
dst_radius.finish();
dst_curves.update_curve_types();
return dst_curves;
}
} // namespace blender::ed::greasepencil

View File

@@ -558,4 +558,22 @@ void draw_grease_pencil_strokes(const RegionView3D &rv3d,
} // namespace image_render
/**
* Create new strokes tracing the rendered outline of existing strokes.
* \param drawing: Drawing with input strokes.
* \param strokes: Selection curves to trace.
* \param transform: Transform to apply to strokes.
* \param corner_subdivisions: Subdivisions for corners and start/end cap.
* \param outline_radius: Radius of the new outline strokes.
* \param outline_offset: Offset of the outline from the original stroke.
* \param material_index: The material index for the new outline strokes.
*/
bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &drawing,
const IndexMask &strokes,
const float4x4 &transform,
int corner_subdivisions,
float outline_radius,
float outline_offset,
int material_index);
} // namespace blender::ed::greasepencil

View File

@@ -15,8 +15,8 @@
#include "BLI_math_vector.hh"
#include "BLI_offset_indices.hh"
#include "BLI_span.hh"
#include "BLI_virtual_array.hh"
#include "DNA_defaults.h"
#include "DNA_modifier_types.h"
#include "DNA_scene_types.h"
@@ -33,6 +33,8 @@
#include "DEG_depsgraph_query.hh"
#include "ED_grease_pencil.hh"
#include "GEO_resample_curves.hh"
#include "UI_interface.hh"
@@ -170,388 +172,6 @@ static int find_closest_point(const Span<float3> positions, const float3 &target
return closest_i;
}
/* Generate points in an counter-clockwise arc between two directions. */
static void generate_arc_from_point_to_point(const float3 &from,
const float3 &to,
const float3 &center_pt,
const int subdivisions,
const int src_point_index,
Vector<float3> &r_perimeter,
Vector<int> &r_src_indices)
{
const float3 vec_from = from - center_pt;
const float3 vec_to = to - center_pt;
if (math::is_zero(vec_from) || math::is_zero(vec_to)) {
r_perimeter.append(center_pt);
r_src_indices.append(src_point_index);
return;
}
const float cos_angle = math::dot(vec_from.xy(), vec_to.xy());
const float sin_angle = vec_from.x * vec_to.y - vec_from.y * vec_to.x;
/* Compute angle in range [0, 2pi) so that the rotation is always counter-clockwise. */
const float angle = math::atan2(-sin_angle, -cos_angle) + M_PI;
/* Number of points is 2^(n+1) + 1 on half a circle (n=subdivisions)
* so we multiply by (angle / pi) to get the right amount of
* points to insert. */
const int num_full = (1 << (subdivisions + 1)) + 1;
const int num_points = num_full * math::abs(angle) / M_PI;
if (num_points < 2) {
r_perimeter.append(center_pt + vec_from);
r_src_indices.append(src_point_index);
return;
}
const float delta_angle = angle / float(num_points - 1);
const float delta_cos = math::cos(delta_angle);
const float delta_sin = math::sin(delta_angle);
float3 vec = vec_from;
for ([[maybe_unused]] const int i : IndexRange(num_points)) {
r_perimeter.append(center_pt + vec);
r_src_indices.append(src_point_index);
const float x = delta_cos * vec.x - delta_sin * vec.y;
const float y = delta_sin * vec.x + delta_cos * vec.y;
vec = float3(x, y, 0.0f);
}
}
/* Generate a semi-circle around a point, opposite the direction. */
static void generate_cap(const float3 &point,
const float3 &tangent,
const float radius,
const int subdivisions,
const eGPDstroke_Caps cap_type,
const int src_point_index,
Vector<float3> &r_perimeter,
Vector<int> &r_src_indices)
{
const float3 normal = {tangent.y, -tangent.x, 0.0f};
switch (cap_type) {
case GP_STROKE_CAP_ROUND:
generate_arc_from_point_to_point(point - normal * radius,
point + normal * radius,
point,
subdivisions,
src_point_index,
r_perimeter,
r_src_indices);
break;
case GP_STROKE_CAP_FLAT:
r_perimeter.append(point + normal * radius);
r_src_indices.append(src_point_index);
break;
case GP_STROKE_CAP_MAX:
BLI_assert_unreachable();
break;
}
}
/* Generate a corner between two segments, with a rounded outer perimeter.
* 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 int subdivisions,
const int src_point_index,
Vector<float3> &r_perimeter,
Vector<int> &r_src_indices)
{
const float length = math::length(pt_c - pt_b);
const float length_prev = math::length(pt_b - pt_a);
const float2 tangent = math::normalize((pt_c - pt_b).xy());
const float2 tangent_prev = math::normalize((pt_b - pt_a).xy());
const float3 normal = {tangent.y, -tangent.x, 0.0f};
const float3 normal_prev = {tangent_prev.y, -tangent_prev.x, 0.0f};
const float sin_angle = tangent_prev.x * tangent.y - tangent_prev.y * tangent.x;
/* 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) {
generate_arc_from_point_to_point(pt_b + normal_prev * radius,
pt_b + normal * radius,
pt_b,
subdivisions,
src_point_index,
r_perimeter,
r_src_indices);
}
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;
r_perimeter.append(miter_point);
r_src_indices.append(src_point_index);
}
}
static void generate_stroke_perimeter(const Span<float3> all_positions,
const VArray<float> all_radii,
const IndexRange points,
const int subdivisions,
const bool is_cyclic,
const bool use_caps,
const eGPDstroke_Caps start_cap_type,
const eGPDstroke_Caps end_cap_type,
const float radius_offset,
Vector<float3> &r_perimeter,
Vector<int> &r_point_counts,
Vector<int> &r_point_indices)
{
const Span<float3> positions = all_positions.slice(points);
const int point_num = points.size();
if (point_num < 2) {
return;
}
auto add_corner = [&](const int a, const int b, const int c) {
const int point = points[b];
const float3 pt_a = positions[a];
const float3 pt_b = positions[b];
const float3 pt_c = positions[c];
const float radius = std::max(all_radii[point] + radius_offset, 0.0f);
generate_corner(pt_a, pt_b, pt_c, radius, 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];
const float3 &center = positions[center_i];
const float3 dir = math::normalize(positions[next_i] - center);
const float radius = std::max(all_radii[point] + radius_offset, 0.0f);
generate_cap(center, dir, radius, subdivisions, cap_type, point, r_perimeter, r_point_indices);
};
/* Creates a single cyclic curve with end caps. */
if (use_caps) {
/* Open curves generate a start and end cap and a connecting stroke on either side. */
const int perimeter_start = r_perimeter.size();
/* Start cap. */
add_cap(0, 1, start_cap_type);
/* Right perimeter half. */
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(i - 1, i, i + 1);
}
if (is_cyclic) {
add_corner(point_num - 2, point_num - 1, 0);
}
/* End cap. */
if (is_cyclic) {
/* End point is same as start point. */
add_cap(0, point_num - 1, end_cap_type);
}
else {
/* End point is last point of the curve. */
add_cap(point_num - 1, point_num - 2, end_cap_type);
}
/* Left perimeter half. */
if (is_cyclic) {
add_corner(0, point_num - 1, point_num - 2);
}
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(point_num - i, point_num - i - 1, point_num - i - 2);
}
const int perimeter_count = r_perimeter.size() - perimeter_start;
if (perimeter_count > 0) {
r_point_counts.append(perimeter_count);
}
}
else {
/* Generate separate "inside" and an "outside" perimeter curves.
* The distinction is arbitrary, called left/right here. */
/* Right side perimeter. */
const int left_perimeter_start = r_perimeter.size();
add_corner(point_num - 1, 0, 1);
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(i - 1, i, i + 1);
}
add_corner(point_num - 2, point_num - 1, 0);
const int left_perimeter_count = r_perimeter.size() - left_perimeter_start;
if (left_perimeter_count > 0) {
r_point_counts.append(left_perimeter_count);
}
/* Left side perimeter. */
const int right_perimeter_start = r_perimeter.size();
add_corner(0, point_num - 1, point_num - 2);
for (const int i : points.index_range().drop_front(1).drop_back(1)) {
add_corner(point_num - i, point_num - i - 1, point_num - i - 2);
}
add_corner(1, 0, point_num - 1);
const int right_perimeter_count = r_perimeter.size() - right_perimeter_start;
if (right_perimeter_count > 0) {
r_point_counts.append(right_perimeter_count);
}
}
}
struct PerimeterData {
/* New points per curve count. */
Vector<int> point_counts;
/* New point coordinates. */
Vector<float3> positions;
/* Source curve index. */
Vector<int> curve_indices;
/* Source point index. */
Vector<int> point_indices;
};
static bke::CurvesGeometry create_curves_outline(const bke::greasepencil::Drawing &drawing,
const float4x4 &viewmat,
const IndexMask &curves_mask,
const int subdivisions,
const float stroke_radius,
int stroke_mat_nr,
const bool keep_shape)
{
const bke::CurvesGeometry &src_curves = drawing.strokes();
Span<float3> src_positions = src_curves.positions();
bke::AttributeAccessor src_attributes = src_curves.attributes();
const VArray<float> src_radii = drawing.radii();
const VArray<bool> src_cyclic = *src_attributes.lookup_or_default(
"cyclic", bke::AttrDomain::Curve, false);
const VArray<int8_t> src_start_caps = *src_attributes.lookup_or_default<int8_t>(
"start_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_ROUND);
const VArray<int8_t> src_end_caps = *src_attributes.lookup_or_default<int8_t>(
"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, -1);
/* Transform positions into view space. */
Array<float3> view_positions(src_positions.size());
threading::parallel_for(view_positions.index_range(), 4096, [&](const IndexRange range) {
for (const int i : range) {
view_positions[i] = math::transform_point(viewmat, src_positions[i]);
}
});
const float4x4 viewinv = math::invert(viewmat);
threading::EnumerableThreadSpecific<PerimeterData> thread_data;
curves_mask.foreach_index([&](const int64_t curve_i) {
PerimeterData &data = thread_data.local();
const bool is_cyclic_curve = src_cyclic[curve_i];
/* NOTE: Cyclic curves would better be represented by a cyclic perimeter without end caps, but
* we always generate caps for compatibility with GPv2. Fill materials cannot create holes, so
* a cyclic outline does not work well. */
const bool use_caps = true /*!is_cyclic_curve*/;
const int prev_point_num = data.positions.size();
const int prev_curve_num = data.point_counts.size();
const IndexRange points = src_curves.points_by_curve()[curve_i];
/* Offset the strokes by the radius so the outside aligns with the input stroke. */
const float radius_offset = keep_shape ? -stroke_radius : 0.0f;
generate_stroke_perimeter(view_positions,
src_radii,
points,
subdivisions,
is_cyclic_curve,
use_caps,
eGPDstroke_Caps(src_start_caps[curve_i]),
eGPDstroke_Caps(src_end_caps[curve_i]),
radius_offset,
data.positions,
data.point_counts,
data.point_indices);
/* Transform perimeter positions back into object space. */
for (float3 &pos : data.positions.as_mutable_span().drop_front(prev_point_num)) {
pos = math::transform_point(viewinv, pos);
}
data.curve_indices.append_n_times(curve_i, data.point_counts.size() - prev_curve_num);
});
int dst_curve_num = 0;
int dst_point_num = 0;
for (const PerimeterData &data : thread_data) {
BLI_assert(data.point_counts.size() == data.curve_indices.size());
BLI_assert(data.positions.size() == data.point_indices.size());
dst_curve_num += data.point_counts.size();
dst_point_num += data.positions.size();
}
bke::CurvesGeometry dst_curves(dst_point_num, dst_curve_num);
if (dst_point_num == 0 || dst_curve_num == 0) {
return dst_curves;
}
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
bke::SpanAttributeWriter<bool> dst_cyclic = dst_attributes.lookup_or_add_for_write_span<bool>(
"cyclic", bke::AttrDomain::Curve);
bke::SpanAttributeWriter<int> dst_material = dst_attributes.lookup_or_add_for_write_span<int>(
"material_index", bke::AttrDomain::Curve);
bke::SpanAttributeWriter<float> dst_radius = dst_attributes.lookup_or_add_for_write_span<float>(
"radius", bke::AttrDomain::Point);
const MutableSpan<int> dst_offsets = dst_curves.offsets_for_write();
const MutableSpan<float3> dst_positions = dst_curves.positions_for_write();
/* Source indices for attribute mapping. */
Array<int> dst_curve_map(dst_curve_num);
Array<int> dst_point_map(dst_point_num);
IndexRange curves;
IndexRange points;
for (const PerimeterData &data : thread_data) {
curves = curves.after(data.point_counts.size());
points = points.after(data.positions.size());
/* Append curve data. */
dst_curve_map.as_mutable_span().slice(curves).copy_from(data.curve_indices);
/* Curve offsets are accumulated below. */
dst_offsets.slice(curves).copy_from(data.point_counts);
dst_cyclic.span.slice(curves).fill(true);
if (stroke_mat_nr >= 0) {
dst_material.span.slice(curves).fill(stroke_mat_nr);
}
else {
for (const int i : curves.index_range()) {
dst_material.span[curves[i]] = src_material_index[data.curve_indices[i]];
}
}
/* Append point data. */
dst_positions.slice(points).copy_from(data.positions);
dst_point_map.as_mutable_span().slice(points).copy_from(data.point_indices);
dst_radius.span.slice(points).fill(stroke_radius);
}
offset_indices::accumulate_counts_to_offsets(dst_curves.offsets_for_write());
bke::gather_attributes(src_attributes,
bke::AttrDomain::Point,
{},
{"position", "radius"},
dst_point_map,
dst_attributes);
bke::gather_attributes(src_attributes,
bke::AttrDomain::Curve,
{},
{"cyclic", "material_index"},
dst_curve_map,
dst_attributes);
dst_cyclic.finish();
dst_material.finish();
dst_radius.finish();
dst_curves.update_curve_types();
return dst_curves;
}
static void modify_drawing(const GreasePencilOutlineModifierData &omd,
const ModifierEvalContext &ctx,
bke::greasepencil::Drawing &drawing,
@@ -570,14 +190,17 @@ static void modify_drawing(const GreasePencilOutlineModifierData &omd,
const float object_scale = math::length(
math::transform_direction(ctx.object->object_to_world(), float3(M_SQRT1_3)));
/* Legacy thickness setting is diameter in pixels, divide by 2000 to get radius. */
const float radius = math::max(omd.thickness * object_scale, 1.0f) * 0.0005f;
const bool keep_shape = omd.flag & MOD_GREASE_PENCIL_OUTLINE_KEEP_SHAPE;
const float radius = math::max(omd.thickness * object_scale, 1.0f) /
bke::greasepencil::LEGACY_RADIUS_CONVERSION_FACTOR;
/* Offset the strokes by the radius so the outside aligns with the input stroke. */
const float outline_offset = (omd.flag & MOD_GREASE_PENCIL_OUTLINE_KEEP_SHAPE) != 0 ? -radius :
0.0f;
const int mat_nr = (omd.outline_material ?
BKE_object_material_index_get(ctx.object, omd.outline_material) :
-1);
bke::CurvesGeometry curves = create_curves_outline(
drawing, viewmat, curves_mask, omd.subdiv, radius, mat_nr, keep_shape);
bke::CurvesGeometry curves = ed::greasepencil::create_curves_outline(
drawing, curves_mask, viewmat, omd.subdiv, radius, outline_offset, mat_nr);
/* Cyclic curve reordering feature. */
if (omd.object) {