diff options
author | Hans Goudey <h.goudey@me.com> | 2022-04-01 16:11:58 +0300 |
---|---|---|
committer | Hans Goudey <h.goudey@me.com> | 2022-04-01 16:12:41 +0300 |
commit | 00ba51d37bf5b152176409b393eafbb0ad9333e6 (patch) | |
tree | 499a1503c59f3558101bba52cef10d64c39a57fd | |
parent | a250d3d1b7d8d497c21a1ef845e64f07e68beda9 (diff) |
Geometry Nodes: Port set handle nodes to new data-block
This commit ports the "Set Handle Positions" and "Set Hanle Type"
nodes to use the new curves data-block. The nodes become simpler
and likely much faster too, though they're usually not the bottleneck
anyway.
Most of the code is ported from `BezierSpline` directly. The majority
of the complexity comes from the interaction between different
automatically calculated handle types. In comparison `BezierSpline`,
the calculation of auto handles is done eagerly-- mostly because it's
simpler. Eventually lazy calculation might be good to add.
Differential Revision: https://developer.blender.org/D14464
6 files changed, 335 insertions, 141 deletions
diff --git a/source/blender/blenkernel/BKE_curves.hh b/source/blender/blenkernel/BKE_curves.hh index 67671e46ad4..f097acc497f 100644 --- a/source/blender/blenkernel/BKE_curves.hh +++ b/source/blender/blenkernel/BKE_curves.hh @@ -338,6 +338,8 @@ class CurvesGeometry : public ::CurvesGeometry { void translate(const float3 &translation); void transform(const float4x4 &matrix); + void calculate_bezier_auto_handles(); + void update_customdata_pointers(); void remove_curves(IndexMask curves_to_delete); @@ -397,9 +399,37 @@ void calculate_evaluated_offsets(Span<int8_t> handle_types_left, MutableSpan<int> evaluated_offsets); /** + * Recalculate all auto (#BEZIER_HANDLE_AUTO) and vector (#BEZIER_HANDLE_VECTOR) handles with + * positions automatically derived from the neighboring control points, and update aligned + * (#BEZIER_HANDLE_ALIGN) handles to line up with neighboring non-aligned handles. The choices + * made here are relatively arbitrary, but having standardized behavior is essential. + */ +void calculate_auto_handles(bool cyclic, + Span<int8_t> types_left, + Span<int8_t> types_right, + Span<float3> positions, + MutableSpan<float3> positions_left, + MutableSpan<float3> positions_right); + +/** + * Change the handles of a single control point, aligning any aligned (#BEZIER_HANDLE_ALIGN) + * handles on the other side of the control point. + * + * \note This ignores the inputs if the handle types are automatically calculated, + * so the types should be updated before-hand to be editable. + */ +void set_handle_position(const float3 &position, + HandleType type, + HandleType type_other, + const float3 &new_handle, + float3 &handle, + float3 &handle_other); + +/** * Evaluate a cubic Bezier segment, using the "forward differencing" method. - * A generic Bezier curve is made up by four points, but in many cases the first and last points - * are referred to as the control points, and the middle points are the corresponding handles. + * A generic Bezier curve is made up by four points, but in many cases the first and last + * points are referred to as the control points, and the middle points are the corresponding + * handles. */ void evaluate_segment(const float3 &point_0, const float3 &point_1, diff --git a/source/blender/blenkernel/intern/curve_bezier.cc b/source/blender/blenkernel/intern/curve_bezier.cc index 0d3bb2e3a7d..30a5869c976 100644 --- a/source/blender/blenkernel/intern/curve_bezier.cc +++ b/source/blender/blenkernel/intern/curve_bezier.cc @@ -58,6 +58,131 @@ void calculate_evaluated_offsets(const Span<int8_t> handle_types_left, evaluated_offsets.last() = offset; } +static float3 calculate_aligned_handle(const float3 &position, + const float3 &other_handle, + const float3 &aligned_handle) +{ + /* Keep track of the old length of the opposite handle. */ + const float length = math::distance(aligned_handle, position); + /* Set the other handle to directly opposite from the current handle. */ + const float3 dir = math::normalize(other_handle - position); + return position - dir * length; +} + +static void calculate_point_handles(const HandleType type_left, + const HandleType type_right, + const float3 position, + const float3 prev_position, + const float3 next_position, + float3 &left, + float3 &right) +{ + if (ELEM(BEZIER_HANDLE_AUTO, type_left, type_right)) { + const float3 prev_diff = position - prev_position; + const float3 next_diff = next_position - position; + float prev_len = math::length(prev_diff); + float next_len = math::length(next_diff); + if (prev_len == 0.0f) { + prev_len = 1.0f; + } + if (next_len == 0.0f) { + next_len = 1.0f; + } + const float3 dir = next_diff / next_len + prev_diff / prev_len; + + /* This magic number is unfortunate, but comes from elsewhere in Blender. */ + const float len = math::length(dir) * 2.5614f; + if (len != 0.0f) { + if (type_left == BEZIER_HANDLE_AUTO) { + const float prev_len_clamped = std::min(prev_len, next_len * 5.0f); + left = position + dir * -(prev_len_clamped / len); + } + if (type_right == BEZIER_HANDLE_AUTO) { + const float next_len_clamped = std::min(next_len, prev_len * 5.0f); + right = position + dir * (next_len_clamped / len); + } + } + } + + if (type_left == BEZIER_HANDLE_VECTOR) { + left = math::interpolate(position, prev_position, 1.0f / 3.0f); + } + + if (type_right == BEZIER_HANDLE_VECTOR) { + right = math::interpolate(position, next_position, 1.0f / 3.0f); + } + + /* When one of the handles is "aligned" handle, it must be aligned with the other, i.e. point in + * the opposite direction. Don't handle the case of two aligned handles, because code elsewhere + * should keep the pair consistent, and the relative locations aren't affected by other points + * anyway. */ + if (type_left == BEZIER_HANDLE_ALIGN && type_right != BEZIER_HANDLE_ALIGN) { + left = calculate_aligned_handle(position, right, left); + } + else if (type_left != BEZIER_HANDLE_ALIGN && type_right == BEZIER_HANDLE_ALIGN) { + right = calculate_aligned_handle(position, left, right); + } +} + +void set_handle_position(const float3 &position, + const HandleType type, + const HandleType type_other, + const float3 &new_handle, + float3 &handle, + float3 &handle_other) +{ + /* Don't bother when the handle positions are calculated automatically anyway. */ + if (ELEM(type, BEZIER_HANDLE_AUTO, BEZIER_HANDLE_VECTOR)) { + return; + } + + handle = new_handle; + if (type_other == BEZIER_HANDLE_ALIGN) { + handle_other = calculate_aligned_handle(position, handle, handle_other); + } +} + +void calculate_auto_handles(const bool cyclic, + const Span<int8_t> types_left, + const Span<int8_t> types_right, + const Span<float3> positions, + MutableSpan<float3> positions_left, + MutableSpan<float3> positions_right) +{ + const int points_num = positions.size(); + if (points_num == 1) { + return; + } + + calculate_point_handles(HandleType(types_left.first()), + HandleType(types_right.first()), + positions.first(), + cyclic ? positions.last() : 2.0f * positions.first() - positions[1], + positions[1], + positions_left.first(), + positions_right.first()); + + threading::parallel_for(IndexRange(1, points_num - 2), 1024, [&](IndexRange range) { + for (const int i : range) { + calculate_point_handles(HandleType(types_left[i]), + HandleType(types_right[i]), + positions[i], + positions[i - 1], + positions[i + 1], + positions_left[i], + positions_right[i]); + } + }); + + calculate_point_handles(HandleType(types_left.last()), + HandleType(types_right.last()), + positions.last(), + positions.last(1), + cyclic ? positions.first() : 2.0f * positions.last() - positions.last(1), + positions_left.last(), + positions_right.last()); +} + void evaluate_segment(const float3 &point_0, const float3 &point_1, const float3 &point_2, diff --git a/source/blender/blenkernel/intern/curves_geometry.cc b/source/blender/blenkernel/intern/curves_geometry.cc index 94402f0e548..66088714e63 100644 --- a/source/blender/blenkernel/intern/curves_geometry.cc +++ b/source/blender/blenkernel/intern/curves_geometry.cc @@ -851,6 +851,34 @@ static void transform_positions(MutableSpan<float3> positions, const float4x4 &m }); } +void CurvesGeometry::calculate_bezier_auto_handles() +{ + const VArray<int8_t> types = std::as_const(*this).curve_types(); + if (types.is_single() && types.get_internal_single() != CURVE_TYPE_BEZIER) { + return; + } + const VArray<bool> cyclic = std::as_const(*this).cyclic(); + const Span<int8_t> types_left = this->handle_types_left(); + const Span<int8_t> types_right = this->handle_types_right(); + const Span<float3> positions = this->positions(); + MutableSpan<float3> positions_left = this->handle_positions_left(); + MutableSpan<float3> positions_right = this->handle_positions_right(); + + threading::parallel_for(this->curves_range(), 128, [&](IndexRange range) { + for (const int i_curve : range) { + if (types[i_curve] == CURVE_TYPE_BEZIER) { + const IndexRange points = this->points_for_curve(i_curve); + curves::bezier::calculate_auto_handles(cyclic[i_curve], + types_left.slice(points), + types_right.slice(points), + positions.slice(points), + positions_left.slice(points), + positions_right.slice(points)); + } + } + }); +} + void CurvesGeometry::translate(const float3 &translation) { /* Use `as_const` because the non-const functions can add the handle attributes. */ diff --git a/source/blender/nodes/geometry/nodes/node_geo_curve_set_handle_type.cc b/source/blender/nodes/geometry/nodes/node_geo_curve_set_handle_type.cc index e8ba78816a5..169f808c473 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_curve_set_handle_type.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_curve_set_handle_type.cc @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: GPL-2.0-or-later */ -#include "BKE_spline.hh" +#include <atomic> + +#include "BKE_curves.hh" #include "UI_interface.h" #include "UI_resources.h" @@ -49,6 +51,33 @@ static HandleType handle_type_from_input_type(GeometryNodeCurveHandleType type) return BEZIER_HANDLE_AUTO; } +static void set_type_in_component(CurveComponent &component, + const GeometryNodeCurveHandleMode mode, + const HandleType new_handle_type, + const Field<bool> &selection_field) +{ + Curves &curves_id = *component.get_for_write(); + bke::CurvesGeometry &curves = bke::CurvesGeometry::wrap(curves_id.geometry); + + GeometryComponentFieldContext field_context{component, ATTR_DOMAIN_POINT}; + fn::FieldEvaluator evaluator{field_context, curves.points_num()}; + evaluator.set_selection(selection_field); + evaluator.evaluate(); + const IndexMask selection = evaluator.get_evaluated_selection_as_mask(); + + if (mode & GEO_NODE_CURVE_HANDLE_LEFT) { + curves.handle_types_left().fill_indices(selection, new_handle_type); + } + if (mode & GEO_NODE_CURVE_HANDLE_RIGHT) { + curves.handle_types_right().fill_indices(selection, new_handle_type); + } + + /* Eagerly calculate automatically derived handle positions if necessary. */ + if (ELEM(new_handle_type, BEZIER_HANDLE_AUTO, BEZIER_HANDLE_VECTOR, BEZIER_HANDLE_ALIGN)) { + curves.calculate_bezier_auto_handles(); + } +} + static void node_geo_exec(GeoNodeExecParams params) { const NodeGeometryCurveSetHandles &storage = node_storage(params.node()); @@ -58,62 +87,33 @@ 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"); - bool has_bezier_spline = false; + const HandleType new_handle_type = handle_type_from_input_type(type); + + std::atomic<bool> has_curves = false; + std::atomic<bool> has_bezier = false; + geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) { if (!geometry_set.has_curves()) { return; } - - /* Retrieve data for write access so we can avoid new allocations for the handles data. */ - CurveComponent &curve_component = geometry_set.get_component_for_write<CurveComponent>(); - std::unique_ptr<CurveEval> curve = curves_to_curve_eval(*curve_component.get_for_read()); - MutableSpan<SplinePtr> splines = curve->splines(); - - GeometryComponentFieldContext field_context{curve_component, ATTR_DOMAIN_POINT}; - const int domain_size = curve_component.attribute_domain_size(ATTR_DOMAIN_POINT); - - fn::FieldEvaluator selection_evaluator{field_context, domain_size}; - selection_evaluator.add(selection_field); - selection_evaluator.evaluate(); - const VArray<bool> &selection = selection_evaluator.get_evaluated<bool>(0); - - const HandleType new_handle_type = handle_type_from_input_type(type); - int point_index = 0; - - for (SplinePtr &spline : splines) { - if (spline->type() != CURVE_TYPE_BEZIER) { - point_index += spline->positions().size(); - continue; - } - - has_bezier_spline = true; - BezierSpline &bezier_spline = static_cast<BezierSpline &>(*spline); - if (ELEM(new_handle_type, BEZIER_HANDLE_FREE, BEZIER_HANDLE_ALIGN)) { - /* In this case the automatically calculated handle types need to be "baked", because - * they're possibly changing from a type that is calculated automatically to a type that - * is positioned manually. */ - bezier_spline.ensure_auto_handles(); - } - - for (int i_point : IndexRange(bezier_spline.size())) { - if (selection[point_index]) { - if (mode & GEO_NODE_CURVE_HANDLE_LEFT) { - bezier_spline.handle_types_left()[i_point] = new_handle_type; - } - if (mode & GEO_NODE_CURVE_HANDLE_RIGHT) { - bezier_spline.handle_types_right()[i_point] = new_handle_type; - } - } - point_index++; - } - bezier_spline.mark_cache_invalid(); + has_curves = true; + const CurveComponent &component = *geometry_set.get_component_for_read<CurveComponent>(); + if (!component.attribute_exists("handle_type_left") || + !component.attribute_exists("handle_type_right")) { + return; } + has_bezier = true; - curve_component.replace(curve_eval_to_curves(*curve)); + set_type_in_component(geometry_set.get_component_for_write<CurveComponent>(), + mode, + new_handle_type, + selection_field); }); - if (!has_bezier_spline) { - params.error_message_add(NodeWarningType::Info, TIP_("No Bezier splines in input curve")); + + if (has_curves && !has_bezier) { + params.error_message_add(NodeWarningType::Info, TIP_("Input curves do not have Bezier type")); } + params.set_output("Curve", std::move(geometry_set)); } } // namespace blender::nodes::node_geo_curve_set_handle_type_cc diff --git a/source/blender/nodes/geometry/nodes/node_geo_set_curve_handles.cc b/source/blender/nodes/geometry/nodes/node_geo_set_curve_handles.cc index 271dd824d27..31b9f1765a5 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_set_curve_handles.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_set_curve_handles.cc @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: GPL-2.0-or-later */ -#include "BKE_spline.hh" +#include <atomic> + +#include "BKE_curves.hh" #include "UI_interface.h" #include "UI_resources.h" @@ -34,8 +36,40 @@ static void node_init(bNodeTree *UNUSED(tree), bNode *node) node->storage = data; } -static void set_position_in_component(const GeometryNodeCurveHandleMode mode, - CurveComponent &component, +static void update_handle_types_for_movement(int8_t &type, int8_t &other) +{ + switch (type) { + case BEZIER_HANDLE_FREE: + break; + case BEZIER_HANDLE_AUTO: + /* Converting auto handles to aligned handled instead of free handles is + * arbitrary, but expected and "standard" based on behavior in edit mode. */ + if (other == BEZIER_HANDLE_AUTO) { + /* Convert pairs of auto handles to aligned handles when moving one side. */ + type = BEZIER_HANDLE_ALIGN; + other = BEZIER_HANDLE_ALIGN; + } + else { + /* If the other handle isn't automatic, just make the handle free. */ + type = BEZIER_HANDLE_FREE; + } + break; + case BEZIER_HANDLE_VECTOR: + type = BEZIER_HANDLE_FREE; + break; + case BEZIER_HANDLE_ALIGN: + /* The handle can stay aligned if the other handle is also aligned (in which case the other + * handle should be updated to be consistent). But otherwise the handle must be made free to + * avoid conflicting with its "aligned" type. */ + if (other != BEZIER_HANDLE_ALIGN) { + type = BEZIER_HANDLE_FREE; + } + break; + } +} + +static void set_position_in_component(CurveComponent &component, + const GeometryNodeCurveHandleMode mode, const Field<bool> &selection_field, const Field<float3> &position_field, const Field<float3> &offset_field) @@ -52,83 +86,44 @@ static void set_position_in_component(const GeometryNodeCurveHandleMode mode, evaluator.add(offset_field); evaluator.evaluate(); const IndexMask selection = evaluator.get_evaluated_selection_as_mask(); - - std::unique_ptr<CurveEval> curve = curves_to_curve_eval(*component.get_for_read()); - - int current_point = 0; - int current_mask = 0; - for (const SplinePtr &spline : curve->splines()) { - if (spline->type() == CURVE_TYPE_BEZIER) { - BezierSpline &bezier = static_cast<BezierSpline &>(*spline); - - bezier.ensure_auto_handles(); - for (const int i : bezier.positions().index_range()) { - if (current_mask < selection.size() && selection[current_mask] == current_point) { - if (mode & GEO_NODE_CURVE_HANDLE_LEFT) { - if (bezier.handle_types_left()[i] == BEZIER_HANDLE_VECTOR) { - bezier.handle_types_left()[i] = BEZIER_HANDLE_FREE; - } - else if (bezier.handle_types_left()[i] == BEZIER_HANDLE_AUTO) { - bezier.handle_types_left()[i] = BEZIER_HANDLE_ALIGN; - } - } - else { - if (bezier.handle_types_right()[i] == BEZIER_HANDLE_VECTOR) { - bezier.handle_types_right()[i] = BEZIER_HANDLE_FREE; - } - else if (bezier.handle_types_right()[i] == BEZIER_HANDLE_AUTO) { - bezier.handle_types_right()[i] = BEZIER_HANDLE_ALIGN; - } - } - current_mask++; - } - current_point++; - } + const VArray<float3> &new_positions = evaluator.get_evaluated<float3>(0); + const VArray<float3> &new_offsets = evaluator.get_evaluated<float3>(1); + + Curves &curves_id = *component.get_for_write(); + bke::CurvesGeometry &curves = bke::CurvesGeometry::wrap(curves_id.geometry); + + Span<float3> positions = curves.positions(); + + const bool use_left = mode == GEO_NODE_CURVE_HANDLE_LEFT; + MutableSpan<int8_t> handle_types = use_left ? curves.handle_types_left() : + curves.handle_types_right(); + MutableSpan<int8_t> handle_types_other = use_left ? curves.handle_types_right() : + curves.handle_types_left(); + MutableSpan<float3> handle_positions = use_left ? curves.handle_positions_left() : + curves.handle_positions_right(); + MutableSpan<float3> handle_positions_other = use_left ? curves.handle_positions_right() : + curves.handle_positions_left(); + + threading::parallel_for(selection.index_range(), 2048, [&](IndexRange range) { + for (const int i : selection.slice(range)) { + update_handle_types_for_movement(handle_types[i], handle_types_other[i]); } - else { - for ([[maybe_unused]] int i : spline->positions().index_range()) { - if (current_mask < selection.size() && selection[current_mask] == current_point) { - current_mask++; - } - current_point++; - } - } - } + }); - const VArray<float3> &positions_input = evaluator.get_evaluated<float3>(0); - const VArray<float3> &offsets_input = evaluator.get_evaluated<float3>(1); - - current_point = 0; - current_mask = 0; - for (const SplinePtr &spline : curve->splines()) { - if (spline->type() == CURVE_TYPE_BEZIER) { - BezierSpline &bezier = static_cast<BezierSpline &>(*spline); - for (const int i : bezier.positions().index_range()) { - if (current_mask < selection.size() && selection[current_mask] == current_point) { - if (mode & GEO_NODE_CURVE_HANDLE_LEFT) { - bezier.set_handle_position_left( - i, positions_input[current_point] + offsets_input[current_point]); - } - else { - bezier.set_handle_position_right( - i, positions_input[current_point] + offsets_input[current_point]); - } - current_mask++; - } - current_point++; - } - } - else { - for ([[maybe_unused]] int i : spline->positions().index_range()) { - if (current_mask < selection.size() && selection[current_mask] == current_point) { - current_mask++; - } - current_point++; - } + threading::parallel_for(selection.index_range(), 2048, [&](IndexRange range) { + for (const int i : selection.slice(range)) { + bke::curves::bezier::set_handle_position(positions[i], + HandleType(handle_types[i]), + HandleType(handle_types_other[i]), + new_positions[i] + new_offsets[i], + handle_positions[i], + handle_positions_other[i]); } - } + }); - component.replace(curve_eval_to_curves(*curve), GeometryOwnershipType::Owned); + curves.calculate_bezier_auto_handles(); + + curves.tag_positions_changed(); } static void node_geo_exec(GeoNodeExecParams params) @@ -141,24 +136,32 @@ static void node_geo_exec(GeoNodeExecParams params) Field<float3> position_field = params.extract_input<Field<float3>>("Position"); Field<float3> offset_field = params.extract_input<Field<float3>>("Offset"); - bool has_bezier = false; + std::atomic<bool> has_curves = false; + std::atomic<bool> has_bezier = false; + geometry_set.modify_geometry_sets([&](GeometrySet &geometry_set) { - if (geometry_set.has_curves()) { - const std::unique_ptr<CurveEval> curve = curves_to_curve_eval( - *geometry_set.get_curves_for_read()); - has_bezier = curve->has_spline_with_type(CURVE_TYPE_BEZIER); - - set_position_in_component(mode, - geometry_set.get_component_for_write<CurveComponent>(), - selection_field, - position_field, - offset_field); + if (!geometry_set.has_curves()) { + return; + } + has_curves = true; + const CurveComponent &component = *geometry_set.get_component_for_read<CurveComponent>(); + if (!component.attribute_exists("handle_left") || + !component.attribute_exists("handle_right")) { + return; } + has_bezier = true; + + set_position_in_component(geometry_set.get_component_for_write<CurveComponent>(), + mode, + selection_field, + position_field, + offset_field); }); - if (!has_bezier) { - params.error_message_add(NodeWarningType::Info, - TIP_("The input geometry does not contain a Bezier spline")); + + if (has_curves && !has_bezier) { + params.error_message_add(NodeWarningType::Info, TIP_("Input curves do not have Bezier type")); } + params.set_output("Curve", std::move(geometry_set)); } diff --git a/source/blender/nodes/geometry/nodes/node_geo_set_position.cc b/source/blender/nodes/geometry/nodes/node_geo_set_position.cc index eb035aa9b6b..d2ff9753897 100644 --- a/source/blender/nodes/geometry/nodes/node_geo_set_position.cc +++ b/source/blender/nodes/geometry/nodes/node_geo_set_position.cc @@ -7,6 +7,8 @@ #include "DNA_mesh_types.h" #include "DNA_meshdata_types.h" +#include "BKE_curves.hh" + #include "node_geometry_util.hh" namespace blender::nodes::node_geo_set_position_cc { @@ -62,6 +64,9 @@ static void set_computed_position_and_offset(GeometryComponent &component, break; } case GEO_COMPONENT_TYPE_CURVE: { + CurveComponent &curve_component = static_cast<CurveComponent &>(component); + Curves &curves_id = *curve_component.get_for_write(); + bke::CurvesGeometry &curves = bke::CurvesGeometry::wrap(curves_id.geometry); if (component.attribute_exists("handle_right") && component.attribute_exists("handle_left")) { OutputAttribute_Typed<float3> handle_right_attribute = @@ -90,6 +95,9 @@ static void set_computed_position_and_offset(GeometryComponent &component, handle_right_attribute.save(); handle_left_attribute.save(); + + /* Automatic Bezier handles must be recalculated based on the new positions. */ + curves.calculate_bezier_auto_handles(); break; } else { |