From a57c3558cdff15fda115cde4cd28aa20dc5425b3 Mon Sep 17 00:00:00 2001 From: Mattias Fredriksson Date: Wed, 6 Aug 2025 13:59:10 +0200 Subject: [PATCH] Curves: Unit tests for curves::nurbs::calculate_evaluated_num() Unit tests veryfying expectation for curves::nurbs::calculate_evaluated_num(). Expectation is computed from closed form expressions rather then hard coded values. Purpose for this is to make the tests easier to adjust if, for example, parameter sampling pattern is changed. It should also make them easier to read and understand. Additional purpose is to create a baseline and verify changes for #144000. Implementation is essentially examples from: https://link.springer.com/book/10.1007/978-3-642-59223-2 Pull Request: https://projects.blender.org/blender/blender/pulls/143920 --- .../blenkernel/intern/curves_geometry_test.cc | 104 ++++++++++++++++++ tests/gtests/testing/testing.h | 13 +++ 2 files changed, 117 insertions(+) diff --git a/source/blender/blenkernel/intern/curves_geometry_test.cc b/source/blender/blenkernel/intern/curves_geometry_test.cc index 2d956a43567..08c1eeed4fe 100644 --- a/source/blender/blenkernel/intern/curves_geometry_test.cc +++ b/source/blender/blenkernel/intern/curves_geometry_test.cc @@ -478,6 +478,110 @@ TEST(curves_geometry, BezierGenericEvaluation) } } +/* -------------------------------------------------------------------- */ +/** \name NURBS: Basis Cache Calculation + * \{ */ + +TEST(curves_geometry, BasisCacheBezierSegmentDeg2) +{ + const int order = 3; + const int point_count = 3; + const int resolution = 3; + const bool is_cyclic = false; + + const std::array knots_data{0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f}; + const Span knots = Span(knots_data); + + /* Expectation */ + auto fn_Ni2_span = [](MutableSpan Ni2, const float u) { + const float nu = 1.0f - u; + Ni2[0] = nu * nu; + Ni2[1] = 2.0f * u * nu; + Ni2[2] = u * u; + }; + + std::array expected_data; + MutableSpan expectation = MutableSpan(expected_data); + fn_Ni2_span(expectation.slice(0, 3), 0.0f); + fn_Ni2_span(expectation.slice(3, 3), 1.0f / 3.0f); + fn_Ni2_span(expectation.slice(6, 3), 2.0f / 3.0f); + fn_Ni2_span(expectation.slice(9, 3), 1.0f); + + /* Test */ + const int evaluated_num = curves::nurbs::calculate_evaluated_num( + point_count, order, is_cyclic, resolution, KnotsMode::NURBS_KNOT_MODE_CUSTOM, knots); + EXPECT_EQ(evaluated_num, resolution + 1); + + curves::nurbs::BasisCache cache; + curves::nurbs::calculate_basis_cache( + point_count, evaluated_num, order, resolution, is_cyclic, knots, cache); + EXPECT_EQ_SPAN(expectation, cache.weights); +} + +TEST(curves_geometry, BasisCacheNonUniformDeg2) +{ + const int order = 3; + const int point_count = 8; + const int resolution = 3; + const bool is_cyclic = false; + + const std::array knots_data{ + 0.0f, 0.0f, 0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 4.0f, 5.0f, 5.0f, 5.0f}; + const Span knots = Span(knots_data); + + /* Expectation */ + auto fn_Ni2_span0 = [](MutableSpan Ni2, const float u) { + Ni2[0] = square_f(1.0f - u); + Ni2[1] = 2.0f * u - 1.5f * square_f(u); + Ni2[2] = square_f(u) / 2.0f; + }; + auto fn_Ni2_span1 = [](MutableSpan Ni2, float u) { + Ni2[0] = square_f(2.0f - u) / 2.0f; + Ni2[1] = -1.5f + 3 * u - square_f(u); + Ni2[2] = square_f(u - 1.0f) / 2.0f; + }; + auto fn_Ni2_span2 = [](MutableSpan Ni2, float u) { + Ni2[0] = square_f(3.0f - u) / 2.0f; + Ni2[1] = -5.5f + 5.0f * u - square_f(u); + Ni2[2] = square_f(u - 2.0f) / 2.0f; + }; + auto fn_Ni2_span3 = [](MutableSpan Ni2, float u) { + Ni2[0] = square_f(4.0f - u) / 2.0f; + Ni2[1] = -16.0f + 10.0f * u - 1.5f * square_f(u); + Ni2[2] = square_f(u - 3.0f); + }; + auto fn_Ni2_span4 = [](MutableSpan Ni2, float u) { + Ni2[0] = square_f(5.0f - u); + Ni2[1] = 2.0f * (u - 4.0f) * (5.0f - u); + Ni2[2] = square_f(u - 4.0f); + }; + + std::array expected_data; + MutableSpan expectation = MutableSpan(expected_data); + fn_Ni2_span0(expectation.slice(0, 3), 0.0f); + for (int i = 1; i < 4; i++) { + const float du = i / 3.0f; + const int step = i * 3; + fn_Ni2_span0(expectation.slice(step, 3), du); + fn_Ni2_span1(expectation.slice(step + 9, 3), 1.0f + du); + fn_Ni2_span2(expectation.slice(step + 18, 3), 2.0f + du); + fn_Ni2_span3(expectation.slice(step + 27, 3), 3.0f + du); + fn_Ni2_span4(expectation.slice(step + 36, 3), 4.0f + du); + } + + /* Test */ + const int evaluated_num = curves::nurbs::calculate_evaluated_num( + point_count, order, is_cyclic, resolution, KnotsMode::NURBS_KNOT_MODE_CUSTOM, knots); + EXPECT_EQ(evaluated_num, 5 * resolution + 1); + + curves::nurbs::BasisCache cache; + curves::nurbs::calculate_basis_cache( + point_count, evaluated_num, order, resolution, is_cyclic, knots, cache); + EXPECT_NEAR_SPAN(expectation, cache.weights, 1e-6f); +} + +/** \} */ + TEST(knot_vector, KnotVectorUniform) { constexpr int8_t order = 5; diff --git a/tests/gtests/testing/testing.h b/tests/gtests/testing/testing.h index 861afceacd5..90b6146a14a 100644 --- a/tests/gtests/testing/testing.h +++ b/tests/gtests/testing/testing.h @@ -153,6 +153,19 @@ inline void EXPECT_EQ_SPAN(const blender::Span expected, const blender::Span< } } +template +inline void EXPECT_NEAR_SPAN(const blender::Span expected, + const blender::Span actual, + const U tolerance) +{ + EXPECT_EQ(expected.size(), actual.size()); + if (expected.size() == actual.size()) { + for (const int64_t i : expected.index_range()) { + EXPECT_NEAR(expected[i], actual[i], tolerance) << "Element mismatch at index " << i; + } + } +} + template inline void EXPECT_EQ_ARRAY(const T *expected, const T *actual, const size_t N) {