Curves: Add basic custom normals support #116066

Merged
Hans Goudey merged 20 commits from HooglyBoogly/blender:curves-normals into main 2023-12-21 03:29:28 +01:00
8 changed files with 238 additions and 91 deletions

View File

@ -422,6 +422,13 @@ class CurveLengthFieldInput final : public CurvesFieldInput {
std::optional<AttrDomain> preferred_domain(const bke::CurvesGeometry &curves) const final;
};
bool try_capture_field_on_geometry(MutableAttributeAccessor attributes,
const fn::FieldContext &field_context,
const AttributeIDRef &attribute_id,
AttrDomain domain,
const fn::Field<bool> &selection,
const fn::GField &field);
bool try_capture_field_on_geometry(GeometryComponent &component,
const AttributeIDRef &attribute_id,
AttrDomain domain,

View File

@ -792,23 +792,35 @@ static void rotate_directions_around_axes(MutableSpan<float3> directions,
}
}
static void evaluate_generic_data_for_curve(
const int curve_index,
const IndexRange points,
const VArray<int8_t> &types,
const VArray<bool> &cyclic,
const VArray<int> &resolution,
const Span<int> all_bezier_evaluated_offsets,
const Span<curves::nurbs::BasisCache> nurbs_basis_cache,
const VArray<int8_t> &nurbs_orders,
const Span<float> nurbs_weights,
const GSpan src,
GMutableSpan dst)
static void normalize_span(MutableSpan<float3> data)
{
switch (types[curve_index]) {
for (const int i : data.index_range()) {
data[i] = math::normalize(data[i]);
}
}
/** Data needed to interpolate generic data from control points to evaluated points. */
struct EvalData {
const OffsetIndices<int> points_by_curve;
const VArray<int8_t> &types;
const VArray<bool> &cyclic;
const VArray<int> &resolution;
const Span<int> all_bezier_evaluated_offsets;
const Span<curves::nurbs::BasisCache> nurbs_basis_cache;
const VArray<int8_t> &nurbs_orders;
const Span<float> nurbs_weights;
};
static void evaluate_generic_data_for_curve(const EvalData &eval_data,
const int curve_index,
const GSpan src,
GMutableSpan dst)
{
const IndexRange points = eval_data.points_by_curve[curve_index];
switch (eval_data.types[curve_index]) {
case CURVE_TYPE_CATMULL_ROM:
curves::catmull_rom::interpolate_to_evaluated(
src, cyclic[curve_index], resolution[curve_index], dst);
src, eval_data.cyclic[curve_index], eval_data.resolution[curve_index], dst);
break;
case CURVE_TYPE_POLY:
dst.copy_from(src);
@ -816,13 +828,13 @@ static void evaluate_generic_data_for_curve(
case CURVE_TYPE_BEZIER: {
const IndexRange offsets = curves::per_curve_point_offsets_range(points, curve_index);
curves::bezier::interpolate_to_evaluated(
src, all_bezier_evaluated_offsets.slice(offsets), dst);
src, eval_data.all_bezier_evaluated_offsets.slice(offsets), dst);
break;
}
case CURVE_TYPE_NURBS:
curves::nurbs::interpolate_to_evaluated(nurbs_basis_cache[curve_index],
nurbs_orders[curve_index],
nurbs_weights.slice_safe(points),
curves::nurbs::interpolate_to_evaluated(eval_data.nurbs_basis_cache[curve_index],
eval_data.nurbs_orders[curve_index],
eval_data.nurbs_weights.slice_safe(points),
src,
dst);
break;
@ -839,19 +851,30 @@ Span<float3> CurvesGeometry::evaluated_normals() const
const VArray<int8_t> types = this->curve_types();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> normal_mode = this->normal_mode();
const VArray<int> resolution = this->resolution();
const VArray<int8_t> nurbs_orders = this->nurbs_orders();
const Span<float> nurbs_weights = this->nurbs_weights();
const Span<int> all_bezier_offsets = runtime.evaluated_offsets_cache.data().all_bezier_offsets;
const Span<curves::nurbs::BasisCache> nurbs_basis_cache = runtime.nurbs_basis_cache.data();
const Span<float3> evaluated_tangents = this->evaluated_tangents();
const AttributeAccessor attributes = this->attributes();
const EvalData eval_data{
points_by_curve,
types,
cyclic,
this->resolution(),
runtime.evaluated_offsets_cache.data().all_bezier_offsets,
runtime.nurbs_basis_cache.data(),
this->nurbs_orders(),
this->nurbs_weights(),
};
const VArray<float> tilt = this->tilt();
VArraySpan<float> tilt_span;
const bool use_tilt = !(tilt.is_single() && tilt.get_internal_single() == 0.0f);
if (use_tilt) {
tilt_span = tilt;
}
VArraySpan<float3> custom_normal_span;
if (const VArray<float3> custom_normal = *attributes.lookup<float3>("custom_normal",
AttrDomain::Point))
{
custom_normal_span = custom_normal;
}
r_data.resize(this->evaluated_points_num());
MutableSpan<float3> evaluated_normals = r_data;
@ -862,7 +885,7 @@ Span<float3> CurvesGeometry::evaluated_normals() const
for (const int curve_index : curves_range) {
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
switch (normal_mode[curve_index]) {
switch (NormalMode(normal_mode[curve_index])) {
case NORMAL_MODE_Z_UP:
curves::poly::calculate_normals_z_up(evaluated_tangents.slice(evaluated_points),
evaluated_normals.slice(evaluated_points));
@ -872,6 +895,19 @@ Span<float3> CurvesGeometry::evaluated_normals() const
cyclic[curve_index],
evaluated_normals.slice(evaluated_points));
break;
case NORMAL_MODE_FREE:
if (custom_normal_span.is_empty()) {
curves::poly::calculate_normals_z_up(evaluated_tangents.slice(evaluated_points),
evaluated_normals.slice(evaluated_points));
}
else {
const Span<float3> src = custom_normal_span.slice(points_by_curve[curve_index]);
MutableSpan<float3> dst = evaluated_normals.slice(
evaluated_points_by_curve[curve_index]);
evaluate_generic_data_for_curve(eval_data, curve_index, src, dst);
normalize_span(dst);
}
break;
}
/* If the "tilt" attribute exists, rotate the normals around the tangents by the
@ -885,15 +921,8 @@ Span<float3> CurvesGeometry::evaluated_normals() const
}
else {
evaluated_tilts.reinitialize(evaluated_points.size());
evaluate_generic_data_for_curve(curve_index,
points,
types,
cyclic,
resolution,
all_bezier_offsets,
nurbs_basis_cache,
nurbs_orders,
nurbs_weights,
evaluate_generic_data_for_curve(eval_data,
curve_index,
tilt_span.slice(points),
evaluated_tilts.as_mutable_span());
rotate_directions_around_axes(evaluated_normals.slice(evaluated_points),
@ -912,51 +941,43 @@ void CurvesGeometry::interpolate_to_evaluated(const int curve_index,
GMutableSpan dst) const
{
const bke::CurvesGeometryRuntime &runtime = *this->runtime;
const OffsetIndices points_by_curve = this->points_by_curve();
const IndexRange points = points_by_curve[curve_index];
BLI_assert(src.size() == points.size());
const EvalData eval_data{
this->points_by_curve(),
this->curve_types(),
this->cyclic(),
this->resolution(),
runtime.evaluated_offsets_cache.data().all_bezier_offsets,
runtime.nurbs_basis_cache.data(),
this->nurbs_orders(),
this->nurbs_weights(),
};
BLI_assert(src.size() == this->points_by_curve().size());
BLI_assert(dst.size() == this->evaluated_points_by_curve()[curve_index].size());
evaluate_generic_data_for_curve(curve_index,
points,
this->curve_types(),
this->cyclic(),
this->resolution(),
runtime.evaluated_offsets_cache.data().all_bezier_offsets,
runtime.nurbs_basis_cache.data(),
this->nurbs_orders(),
this->nurbs_weights(),
src,
dst);
evaluate_generic_data_for_curve(eval_data, curve_index, src, dst);
}
void CurvesGeometry::interpolate_to_evaluated(const GSpan src, GMutableSpan dst) const
{
const bke::CurvesGeometryRuntime &runtime = *this->runtime;
const OffsetIndices points_by_curve = this->points_by_curve();
const EvalData eval_data{
points_by_curve,
this->curve_types(),
this->cyclic(),
this->resolution(),
runtime.evaluated_offsets_cache.data().all_bezier_offsets,
runtime.nurbs_basis_cache.data(),
this->nurbs_orders(),
this->nurbs_weights(),
};
const OffsetIndices evaluated_points_by_curve = this->evaluated_points_by_curve();
const VArray<int8_t> types = this->curve_types();
const VArray<int> resolution = this->resolution();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> nurbs_orders = this->nurbs_orders();
const Span<float> nurbs_weights = this->nurbs_weights();
const Span<int> all_bezier_offsets = runtime.evaluated_offsets_cache.data().all_bezier_offsets;
const Span<curves::nurbs::BasisCache> nurbs_basis_cache = runtime.nurbs_basis_cache.data();
threading::parallel_for(this->curves_range(), 512, [&](IndexRange curves_range) {
for (const int curve_index : curves_range) {
const IndexRange points = points_by_curve[curve_index];
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
evaluate_generic_data_for_curve(curve_index,
points,
types,
cyclic,
resolution,
all_bezier_offsets,
nurbs_basis_cache,
nurbs_orders,
nurbs_weights,
src.slice(points),
dst.slice(evaluated_points));
evaluate_generic_data_for_curve(
eval_data, curve_index, src.slice(points), dst.slice(evaluated_points));
}
});
}
@ -1058,6 +1079,16 @@ static void transform_positions(MutableSpan<float3> positions, const float4x4 &m
});
}
HooglyBoogly marked this conversation as resolved Outdated

same as in the other transform normal function in realize_instances.cc.

same as in the other transform normal function in `realize_instances.cc`.
static void transform_normals(MutableSpan<float3> normals, const float4x4 &matrix)
{
const float3x3 normal_transform = math::transpose(math::invert(float3x3(matrix)));
threading::parallel_for(normals.index_range(), 1024, [&](const IndexRange range) {
for (float3 &normal : normals.slice(range)) {
normal = normal_transform * normal;
}
});
}
void CurvesGeometry::calculate_bezier_auto_handles()
{
if (!this->has_curve_with_type(CURVE_TYPE_BEZIER)) {
@ -1126,6 +1157,11 @@ void CurvesGeometry::transform(const float4x4 &matrix)
if (!this->handle_positions_right().is_empty()) {
transform_positions(this->handle_positions_right_for_write(), matrix);
}
MutableAttributeAccessor attributes = this->attributes_for_write();
if (SpanAttributeWriter normals = attributes.lookup_for_write_span<float3>("custom_normal")) {
transform_normals(normals.span, matrix);
normals.finish();
}
this->tag_positions_changed();
}

View File

@ -149,6 +149,9 @@ static Array<float3> curve_normal_point_domain(const bke::CurvesGeometry &curves
const VArray<int8_t> types = curves.curve_types();
const VArray<int> resolutions = curves.resolution();
const VArray<bool> curves_cyclic = curves.cyclic();
const bke::AttributeAccessor attributes = curves.attributes();
const VArray<float3> custom_normals = *attributes.lookup_or_default<float3>(
"custom_normal", AttrDomain::Point, float3(0, 0, 1));
const Span<float3> positions = curves.positions();
const VArray<int8_t> normal_modes = curves.normal_mode();
@ -202,6 +205,9 @@ static Array<float3> curve_normal_point_domain(const bke::CurvesGeometry &curves
case NORMAL_MODE_MINIMUM_TWIST:
bke::curves::poly::calculate_normals_minimum(nurbs_tangents, cyclic, curve_normals);
break;
case NORMAL_MODE_FREE:
custom_normals.materialize(points, curve_normals);
break;
}
break;
}
@ -569,7 +575,7 @@ static ComponentAttributeProviders create_attribute_providers_for_curve()
static const auto normal_mode_clamp = mf::build::SI1_SO<int8_t, int8_t>(
"Normal Mode Validate",
[](int8_t value) {
return std::clamp<int8_t>(value, NORMAL_MODE_MINIMUM_TWIST, NORMAL_MODE_Z_UP);
return std::clamp<int8_t>(value, NORMAL_MODE_MINIMUM_TWIST, NORMAL_MODE_FREE);
},
mf::build::exec_presets::AllSpanOrSingle());
static BuiltinCustomDataLayerProvider normal_mode("normal_mode",
@ -582,6 +588,15 @@ static ComponentAttributeProviders create_attribute_providers_for_curve()
tag_component_normals_changed,
AttributeValidator{&normal_mode_clamp});
static BuiltinCustomDataLayerProvider custom_normal("custom_normal",
AttrDomain::Point,
CD_PROP_FLOAT3,
CD_PROP_FLOAT3,
BuiltinAttributeProvider::Creatable,
BuiltinAttributeProvider::Deletable,
point_access,
tag_component_normals_changed);
static const auto knots_mode_clamp = mf::build::SI1_SO<int8_t, int8_t>(
"Knots Mode Validate",
[](int8_t value) {
@ -650,6 +665,7 @@ static ComponentAttributeProviders create_attribute_providers_for_curve()
&handle_type_right,
&handle_type_left,
&normal_mode,
&custom_normal,
&nurbs_order,
&nurbs_knots_mode,
&nurbs_weight,

View File

@ -664,12 +664,12 @@ static bool try_add_shared_field_attribute(MutableAttributeAccessor attributes,
return attributes.add(id_to_create, domain, data_type, init);
}
static bool try_capture_field_on_geometry(MutableAttributeAccessor attributes,
const GeometryFieldContext &field_context,
const AttributeIDRef &attribute_id,
const AttrDomain domain,
const fn::Field<bool> &selection,
const fn::GField &field)
bool try_capture_field_on_geometry(MutableAttributeAccessor attributes,
const fn::FieldContext &field_context,
const AttributeIDRef &attribute_id,
const AttrDomain domain,
const fn::Field<bool> &selection,
const fn::GField &field)
{
const int domain_size = attributes.domain_size(domain);
const CPPType &type = field.cpp_type();

View File

@ -11,6 +11,7 @@
#include "BLI_listbase.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_rotation.hh"
#include "BLI_noise.hh"
#include "BLI_task.hh"
@ -158,6 +159,9 @@ struct RealizeCurveInfo {
* curves.
*/
Span<float> nurbs_weight;
/** Custom normals are rotated based on each instance's transformation. */
HooglyBoogly marked this conversation as resolved Outdated

Could use a comment explaining why it has special handling.

Could use a comment explaining why it has special handling.
Span<float3> custom_normal;
};
/** Start indices in the final output curves data-block. */
@ -218,6 +222,7 @@ struct AllCurvesInfo {
bool create_radius_attribute = false;
bool create_resolution_attribute = false;
bool create_nurbs_weight_attribute = false;
bool create_custom_normal_attribute = false;
};
/** Collects all tasks that need to be executed to realize all instances. */
@ -292,6 +297,23 @@ static void copy_transformed_positions(const Span<float3> src,
});
}
static void copy_transformed_normals(const Span<float3> src,
const float4x4 &transform,
HooglyBoogly marked this conversation as resolved Outdated

copy_transformed_normals

`copy_transformed_normals`
MutableSpan<float3> dst)
{
const float3x3 normal_transform = math::transpose(math::invert(float3x3(transform)));
if (math::is_equal(normal_transform, float3x3::identity(), 1e-6f)) {
dst.copy_from(src);
}
else {
threading::parallel_for(src.index_range(), 1024, [&](const IndexRange range) {
for (const int i : range) {
dst[i] = normal_transform * src[i];
}
HooglyBoogly marked this conversation as resolved
Review

Might be reasonable to use raw matrix multiplication here, since you did the direction-specific part already above and ended up with a 3x3 matrix.

Might be reasonable to use raw matrix multiplication here, since you did the direction-specific part already above and ended up with a 3x3 matrix.
});
}
}
static void threaded_copy(const GSpan src, GMutableSpan dst)
{
BLI_assert(src.size() == dst.size());
@ -1215,6 +1237,7 @@ static OrderedAttributes gather_generic_curve_attributes_to_propagate(
attributes_to_propagate.remove("resolution");
attributes_to_propagate.remove("handle_right");
attributes_to_propagate.remove("handle_left");
attributes_to_propagate.remove("custom_normal");
r_create_id = attributes_to_propagate.pop_try("id").has_value();
OrderedAttributes ordered_attributes;
for (const auto item : attributes_to_propagate.items()) {
@ -1294,6 +1317,11 @@ static AllCurvesInfo preprocess_curves(const bke::GeometrySet &geometry_set,
.varray.get_internal_span();
info.create_handle_postion_attributes = true;
}
if (attributes.contains("custom_normal")) {
curve_info.custom_normal = attributes.lookup<float3>("custom_normal", bke::AttrDomain::Point)
.varray.get_internal_span();
info.create_custom_normal_attribute = true;
}
}
return info;
}
@ -1309,7 +1337,8 @@ static void execute_realize_curve_task(const RealizeInstancesOptions &options,
MutableSpan<float3> all_handle_right,
MutableSpan<float> all_radii,
MutableSpan<float> all_nurbs_weights,
MutableSpan<int> all_resolutions)
MutableSpan<int> all_resolutions,
MutableSpan<float3> all_custom_normals)
{
const RealizeCurveInfo &curves_info = *task.curve_info;
const Curves &curves_id = *curves_info.curves;
@ -1359,6 +1388,16 @@ static void execute_realize_curve_task(const RealizeInstancesOptions &options,
curves_info.resolution.materialize(all_resolutions.slice(dst_curve_range));
}
if (all_curves_info.create_custom_normal_attribute) {
if (curves_info.custom_normal.is_empty()) {
all_custom_normals.slice(dst_point_range).fill(float3(0, 0, 1));
}
else {
copy_transformed_normals(
curves_info.custom_normal, task.transform, all_custom_normals.slice(dst_point_range));
}
}
/* Copy curve offsets. */
const Span<int> src_offsets = curves.offsets();
const MutableSpan<int> dst_offsets = dst_curves.offsets_for_write().slice(dst_curve_range);
@ -1460,6 +1499,11 @@ static void execute_realize_curve_tasks(const RealizeInstancesOptions &options,
resolution = dst_attributes.lookup_or_add_for_write_only_span<int>("resolution",
bke::AttrDomain::Curve);
}
SpanAttributeWriter<float3> custom_normal;
if (all_curves_info.create_custom_normal_attribute) {
custom_normal = dst_attributes.lookup_or_add_for_write_only_span<float3>(
"custom_normal", bke::AttrDomain::Point);
}
/* Actually execute all tasks. */
threading::parallel_for(tasks.index_range(), 100, [&](const IndexRange task_range) {
@ -1476,7 +1520,8 @@ static void execute_realize_curve_tasks(const RealizeInstancesOptions &options,
handle_right.span,
radius.span,
nurbs_weight.span,
resolution.span);
resolution.span,
custom_normal.span);
}
});
@ -1499,6 +1544,7 @@ static void execute_realize_curve_tasks(const RealizeInstancesOptions &options,
nurbs_weight.finish();
handle_left.finish();
handle_right.finish();
custom_normal.finish();
}
/** \} */

View File

@ -86,6 +86,8 @@ typedef enum NormalMode {
* is vertical, the X axis is used.
*/
NORMAL_MODE_Z_UP = 1,
/** Interpolate the stored "custom_normal" attribute for the final normals. */
NORMAL_MODE_FREE = 2,
} NormalMode;
/**

View File

@ -37,6 +37,11 @@ const EnumPropertyItem rna_enum_curve_normal_mode_items[] = {
"Z Up",
"Calculate normals perpendicular to the Z axis and the curve tangent. If a series of points "
"is vertical, the X axis is used"},
{NORMAL_MODE_FREE,
"FREE",
ICON_NONE,
"Free",
"Use the stored custom normal attribute as the final normals"},
{0, nullptr, 0, nullptr, nullptr},
};

View File

@ -21,6 +21,7 @@ static void node_declare(NodeDeclarationBuilder &b)
b.add_input<decl::Geometry>("Curve").supported_type(
{GeometryComponent::Type::Curve, GeometryComponent::Type::GreasePencil});
b.add_input<decl::Bool>("Selection").default_value(true).hide_value().field_on_all();
b.add_input<decl::Vector>("Normal").default_value({0.0f, 0.0f, 1.0f}).field_on_all();
b.add_output<decl::Geometry>("Curve").propagate_all();
}
HooglyBoogly marked this conversation as resolved Outdated

For this kind of socket we don't use dynamic socket declarations yet, because it's incompatible with the idea of exposing the enum as socket.

For this kind of socket we don't use dynamic socket declarations yet, because it's incompatible with the idea of exposing the enum as socket.

Right, good point

Right, good point
@ -34,22 +35,44 @@ static void node_init(bNodeTree * /*tree*/, bNode *node)
node->custom1 = NORMAL_MODE_MINIMUM_TWIST;
}
static void node_update(bNodeTree *ntree, bNode *node)
{
const NormalMode mode = NormalMode(node->custom1);
bNodeSocket *normal_socket = static_cast<bNodeSocket *>(node->inputs.last);
bke::nodeSetSocketAvailability(ntree, normal_socket, mode == NORMAL_MODE_FREE);
}
static void set_curve_normal(bke::CurvesGeometry &curves,
const NormalMode mode,
const fn::FieldContext field_context,
const Field<bool> &selection_field)
const fn::FieldContext &curve_context,
const fn::FieldContext &point_context,
const Field<bool> &selection_field,
const Field<float3> &custom_normal)
{
fn::FieldEvaluator evaluator{field_context, curves.curves_num()};
evaluator.set_selection(selection_field);
evaluator.evaluate();
const IndexMask selection = evaluator.get_evaluated_selection_as_mask();
index_mask::masked_fill<int8_t>(curves.normal_mode_for_write(), mode, selection);
bke::try_capture_field_on_geometry(curves.attributes_for_write(),
curve_context,
"normal_mode",
AttrDomain::Curve,
selection_field,
fn::make_constant_field<int8_t>(mode));
if (mode == NORMAL_MODE_FREE) {
bke::try_capture_field_on_geometry(
curves.attributes_for_write(),
point_context,
"custom_normal",
AttrDomain::Point,
Field<bool>(std::make_shared<EvaluateOnDomainInput>(selection_field, AttrDomain::Curve)),
custom_normal);
}
curves.tag_normals_changed();
}
static void set_grease_pencil_normal(GreasePencil &grease_pencil,
const NormalMode mode,
const Field<bool> &selection_field)
const Field<bool> &selection_field,
const Field<float3> &custom_normal)
{
using namespace blender::bke::greasepencil;
for (const int layer_index : grease_pencil.layers().index_range()) {
@ -57,10 +80,13 @@ static void set_grease_pencil_normal(GreasePencil &grease_pencil,
if (drawing == nullptr) {
continue;
}
bke::CurvesGeometry &curves = drawing->strokes_for_write();
const bke::GreasePencilLayerFieldContext field_context(
grease_pencil, AttrDomain::Curve, layer_index);
set_curve_normal(curves, mode, field_context, selection_field);
set_curve_normal(
drawing->strokes_for_write(),
mode,
bke::GreasePencilLayerFieldContext(grease_pencil, AttrDomain::Curve, layer_index),
bke::GreasePencilLayerFieldContext(grease_pencil, AttrDomain::Point, layer_index),
selection_field,
custom_normal);
}
}
@ -70,15 +96,23 @@ static void node_geo_exec(GeoNodeExecParams params)
GeometrySet geometry_set = params.extract_input<GeometrySet>("Curve");
Field<bool> selection_field = params.extract_input<Field<bool>>("Selection");
Field<float3> custom_normal;
if (mode == NORMAL_MODE_FREE) {
custom_normal = params.extract_input<Field<float3>>("Normal");
}
geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) {
if (Curves *curves_id = geometry_set.get_curves_for_write()) {
bke::CurvesGeometry &curves = curves_id->geometry.wrap();
const bke::CurvesFieldContext field_context{curves, AttrDomain::Curve};
set_curve_normal(curves, mode, field_context, selection_field);
set_curve_normal(curves,
mode,
bke::CurvesFieldContext(curves, AttrDomain::Curve),
bke::CurvesFieldContext(curves, AttrDomain::Point),
selection_field,
custom_normal);
}
if (geometry_set.has_grease_pencil()) {
set_grease_pencil_normal(*geometry_set.get_grease_pencil_for_write(), mode, selection_field);
if (GreasePencil *grease_pencil = geometry_set.get_grease_pencil_for_write()) {
set_grease_pencil_normal(*grease_pencil, mode, selection_field, custom_normal);
}
});
@ -100,6 +134,7 @@ static void node_register()
static bNodeType ntype;
geo_node_type_base(&ntype, GEO_NODE_SET_CURVE_NORMAL, "Set Curve Normal", NODE_CLASS_GEOMETRY);
ntype.declare = node_declare;
ntype.updatefunc = node_update;
ntype.geometry_node_execute = node_geo_exec;
ntype.initfunc = node_init;
ntype.draw_buttons = node_layout;