From cea51c1bb500eb2cfca425e1cae5dcc419dda2ce Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Fri, 25 Mar 2022 09:03:35 -0500 Subject: [PATCH] Curves: Bezier and general interpolate to evaluated utility This commit implements generic evaluation for Bezier curves (which is really just linear interpolation, since attributes are not stored on Bezier handles). For complete parity with the old curve type, we would have to add options for this (RNA: `Spline.radius_interpolation`), but it's not clear that we want to do that. This also adds a generic `interpolate_to_evaluate` utility on curves that hides the implementation details. Though there is theoretically a performance cost to that, without some abstraction calling code would usually be too complex. Differential Revision: https://developer.blender.org/D14447 --- source/blender/blenkernel/BKE_curves.hh | 16 ++++ .../blender/blenkernel/intern/curve_bezier.cc | 40 ++++++++++ .../blenkernel/intern/curves_geometry.cc | 35 +++++++++ .../blenkernel/intern/curves_geometry_test.cc | 73 +++++++++++++++++++ 4 files changed, 164 insertions(+) diff --git a/source/blender/blenkernel/BKE_curves.hh b/source/blender/blenkernel/BKE_curves.hh index 96963dcbd8d..82f77d83bec 100644 --- a/source/blender/blenkernel/BKE_curves.hh +++ b/source/blender/blenkernel/BKE_curves.hh @@ -266,6 +266,15 @@ class CurvesGeometry : public ::CurvesGeometry { Span evaluated_positions() const; + /** + * Evaluate a generic data to the standard evaluated points of a specific curve, + * defined by the resolution attribute or other factors, depending on the curve type. + * + * \warning This function expects offsets to the evaluated points for each curve to be + * calculated. That can be ensured with #ensure_evaluated_offsets. + */ + void interpolate_to_evaluated(int curve_index, GSpan src, GMutableSpan dst) const; + private: /** * Make sure the basis weights for NURBS curve's evaluated points are calculated. @@ -381,6 +390,13 @@ void calculate_evaluated_positions(Span positions, Span evaluated_offsets, MutableSpan evaluated_positions); +/** + * Evaluate generic data to the evaluated points, with counts for each segment described by + * #evaluated_offsets. Unlike other curve types, for Bezier curves generic data and positions + * are treated separately, since attribute values aren't stored for the handle control points. + */ +void interpolate_to_evaluated(GSpan src, Span evaluated_offsets, GMutableSpan dst); + } // namespace bezier namespace catmull_rom { diff --git a/source/blender/blenkernel/intern/curve_bezier.cc b/source/blender/blenkernel/intern/curve_bezier.cc index c02555dcf6a..8efe7a17a35 100644 --- a/source/blender/blenkernel/intern/curve_bezier.cc +++ b/source/blender/blenkernel/intern/curve_bezier.cc @@ -134,6 +134,46 @@ void calculate_evaluated_positions(const Span positions, } } +template +static inline void linear_interpolation(const T &a, const T &b, MutableSpan dst) +{ + dst.first() = a; + const float step = 1.0f / dst.size(); + for (const int i : dst.index_range().drop_front(1)) { + dst[i] = attribute_math::mix2(i * step, a, b); + } +} + +template +static void interpolate_to_evaluated(const Span src, + const Span evaluated_offsets, + MutableSpan dst) +{ + linear_interpolation(src.first(), src[1], dst.take_front(evaluated_offsets.first())); + + threading::parallel_for( + src.index_range().drop_back(1).drop_front(1), 512, [&](IndexRange range) { + for (const int i : range) { + const IndexRange segment_points = offsets_to_range(evaluated_offsets, i - 1); + linear_interpolation(src[i], src[i + 1], dst.slice(segment_points)); + } + }); + + const IndexRange last_segment_points(evaluated_offsets.last(1), + evaluated_offsets.last() - evaluated_offsets.last(1)); + linear_interpolation(src.last(), src.first(), dst.slice(last_segment_points)); +} + +void interpolate_to_evaluated(const GSpan src, const Span evaluated_offsets, GMutableSpan dst) +{ + attribute_math::convert_to_static_type(src.type(), [&](auto dummy) { + using T = decltype(dummy); + if constexpr (!std::is_void_v>) { + interpolate_to_evaluated(src.typed(), evaluated_offsets, dst.typed()); + } + }); +} + /** \} */ } // namespace blender::bke::curves::bezier diff --git a/source/blender/blenkernel/intern/curves_geometry.cc b/source/blender/blenkernel/intern/curves_geometry.cc index 207d0d173ac..1dfd95ebb5b 100644 --- a/source/blender/blenkernel/intern/curves_geometry.cc +++ b/source/blender/blenkernel/intern/curves_geometry.cc @@ -689,6 +689,41 @@ Span CurvesGeometry::evaluated_positions() const return this->runtime->evaluated_position_cache; } +void CurvesGeometry::interpolate_to_evaluated(const int curve_index, + const GSpan src, + GMutableSpan dst) const +{ + BLI_assert(!this->runtime->offsets_cache_dirty); + BLI_assert(!this->runtime->nurbs_basis_cache_dirty); + const IndexRange points = this->points_for_curve(curve_index); + BLI_assert(src.size() == points.size()); + BLI_assert(dst.size() == this->evaluated_points_for_curve(curve_index).size()); + switch (this->curve_types()[curve_index]) { + case CURVE_TYPE_CATMULL_ROM: + curves::catmull_rom::interpolate_to_evaluated( + src, this->cyclic()[curve_index], this->resolution()[curve_index], dst); + break; + case CURVE_TYPE_POLY: + dst.type().copy_assign_n(src.data(), dst.data(), src.size()); + break; + case CURVE_TYPE_BEZIER: + curves::bezier::interpolate_to_evaluated( + src, this->runtime->bezier_evaluated_offsets.as_span().slice(points), dst); + break; + case CURVE_TYPE_NURBS: + curves::nurbs::interpolate_to_evaluated(this->runtime->nurbs_basis_cache[curve_index], + this->nurbs_orders()[curve_index], + this->nurbs_weights().slice(points), + src, + dst); + break; + default: + BLI_assert_unreachable(); + break; + } +} + + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/blenkernel/intern/curves_geometry_test.cc b/source/blender/blenkernel/intern/curves_geometry_test.cc index 574f90f2e51..e4dc9eead60 100644 --- a/source/blender/blenkernel/intern/curves_geometry_test.cc +++ b/source/blender/blenkernel/intern/curves_geometry_test.cc @@ -405,4 +405,77 @@ TEST(curves_geometry, NURBSEvaluation) } } +TEST(curves_geometry, BezierGenericEvaluation) +{ + CurvesGeometry curves(3, 1); + curves.curve_types().fill(CURVE_TYPE_BEZIER); + curves.resolution().fill(8); + curves.offsets().last() = 3; + + MutableSpan handles_left = curves.handle_positions_left(); + MutableSpan handles_right = curves.handle_positions_right(); + MutableSpan positions = curves.positions(); + positions.first() = {-1, 0, 0}; + handles_right.first() = {-1, 1, 0}; + handles_left[1] = {0, 0, 0}; + positions[1] = {1, 0, 0}; + handles_right[1] = {2, 0, 0}; + handles_left.last() = {1, 1, 0}; + positions.last() = {2, 1, 0}; + + /* Dangling handles shouldn't be used in a non-cyclic curve. */ + handles_left.first() = {100, 100, 100}; + handles_right.last() = {100, 100, 100}; + + Span evaluated_positions = curves.evaluated_positions(); + static const Array result_1{{ + {-1.0f, 0.0f, 0.0f}, + {-0.955078f, 0.287109f, 0.0f}, + {-0.828125f, 0.421875f, 0.0f}, + {-0.630859f, 0.439453f, 0.0f}, + {-0.375f, 0.375f, 0.0f}, + {-0.0722656f, 0.263672f, 0.0f}, + {0.265625f, 0.140625f, 0.0f}, + {0.626953f, 0.0410156f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {1.28906f, 0.0429688f, 0.0f}, + {1.4375f, 0.15625f, 0.0f}, + {1.49219f, 0.316406f, 0.0f}, + {1.5f, 0.5f, 0.0f}, + {1.50781f, 0.683594f, 0.0f}, + {1.5625f, 0.84375f, 0.0f}, + {1.71094f, 0.957031f, 0.0f}, + {2.0f, 1.0f, 0.0f}, + }}; + for (const int i : evaluated_positions.index_range()) { + EXPECT_V3_NEAR(evaluated_positions[i], result_1[i], 1e-5f); + } + + Array radii{{0.0f, 1.0f, 2.0f}}; + Array evaluated_radii(17); + curves.interpolate_to_evaluated(0, radii.as_span(), evaluated_radii.as_mutable_span()); + static const Array result_2{{ + 0.0f, + 0.125f, + 0.25f, + 0.375f, + 0.5f, + 0.625f, + 0.75f, + 0.875f, + 1.0f, + 1.125f, + 1.25f, + 1.375f, + 1.5f, + 1.625f, + 1.75f, + 1.875f, + 2.0f, + }}; + for (const int i : evaluated_radii.index_range()) { + EXPECT_NEAR(evaluated_radii[i], result_2[i], 1e-6f); + } +} + } // namespace blender::bke::tests