diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc index e3a553c7ae0..d4e65143a8e 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc +++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc @@ -82,6 +82,46 @@ static int8_t get_valid_nurbs_degree(const NurbsElement &element) int8_t(degree); } +/** + * Get the number of control points repeated for a cyclic curve given the multiplicity at the end + * of the knot vectors (multiplicity at both ends need to match). + */ +static int cyclic_repeated_points(const int8_t order, const int end_multiplicity) +{ + /* Since multiplicity == order is considered 'cyclic' even if it is only C0 continuous in + * principle (it is technically discontinuous!), it needs to be clamped to 1. + */ + return std::max(order - end_multiplicity, 1); +} + +/** + * Compute the number of occurences of each unique knot value (so knot multiplicity), forming a + * sequence for which: sum(multiplicity) == knots.size(). + * + * Example: + * Knots: [0, 0, 0, 0.1, 0.3, 0.4, 0.4, 0.4] + * Result: [3, 1, 1, 3] + */ +static Vector calculate_multiplicity_sequence(const Span knots) +{ + Vector multiplicity; + multiplicity.reserve(knots.size()); + + int m = 1; + for (const int64_t i : knots.index_range().drop_front(1)) { + /* Only consider multiplicity for exact matching values. */ + if (knots[i - 1] == knots[i]) { + m++; + } + else { + multiplicity.append(m); + m = 1; + } + } + multiplicity.append(m); + return multiplicity; +} + void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import_params) { const NurbsElement &nurbs_geometry = curve_geometry_.nurbs_element_; @@ -99,10 +139,12 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import nurb->orderu = nurb->orderv = degree + 1; nurb->resolu = nurb->resolv = curve->resolu; + const Vector multiplicity = calculate_multiplicity_sequence(nurbs_geometry.parm); nurb->flagu = this->detect_knot_mode(import_params, degree, nurbs_geometry.curv_indices, nurbs_geometry.parm, + multiplicity, nurbs_geometry.range); if ((nurb->flagu & (CU_NURB_CUSTOM | CU_NURB_CYCLIC | CU_NURB_ENDPOINT)) == CU_NURB_CUSTOM) { @@ -111,9 +153,11 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import nurb->flagu &= ~CU_NURB_CUSTOM; } + const int repeated_points = nurb->flagu & CU_NURB_CYCLIC ? + cyclic_repeated_points(nurb->orderu, multiplicity.first()) : + 0; const Span indices = nurbs_geometry.curv_indices.as_span().slice( - nurbs_geometry.curv_indices.index_range().drop_front(nurb->flagu & CU_NURB_CYCLIC ? degree : - 0)); + nurbs_geometry.curv_indices.index_range().drop_back(repeated_points)); BKE_nurb_points_add(nurb, indices.size()); for (const int i : indices.index_range()) { @@ -134,21 +178,103 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import } } -static bool detect_knot_mode_cyclic(const int degree, - const Span indices, - const Span knots) +static bool detect_clamped_endpoint(const int8_t degree, const Span multiplicity) { - const Span indices_tail = indices.take_back(degree); - for (const int i : IndexRange(degree)) { + const int8_t order = degree + 1; + return multiplicity.first() == order && multiplicity.last() == order; +} + +static bool detect_knot_mode_cyclic(const int8_t degree, + const Span indices, + const Span knots, + const Span multiplicity) +{ + constexpr float epsilon = 1e-7; + const int8_t order = degree + 1; + + /* This is a good distinction between the 'cyclic' property and a true periodic NURBS curve. A + * periodic curve should be smooth to the degree - 1 derivative (which is the maximum possible). + * Allowing matching `multiplicity > 1` is not a periodic NURBS but can be considered cyclic. + */ + if (multiplicity.first() != multiplicity.last()) { + return false; + } + + /* Multiplicity m is continous to the `degree - m` derivative and as such multiplcitiy == order + * is discontinous. By allowing it, clamped or Bezier curves can still be considered cyclic but + * ensure [here] that illogical `multiplicities > order` is not considered cyclic. + */ + if (multiplicity.first() > order || multiplicity.last() > order) { + + return false; + } + + const int repeated_points = cyclic_repeated_points(order, multiplicity.first()); + const Span indices_tail = indices.take_back(repeated_points); + for (const int64_t i : indices_tail.index_range()) { if (indices[i] != indices_tail[i]) { return false; } } - const Span knots_tail = knots.take_back(2 * degree + 1); - for (const int i : IndexRange(degree - 1)) { + + if (multiplicity.first() >= degree) { + /* There is no overlap in the knot spans. */ + return true; + } + + /* Ensure it matches on both of the knot spans adjacent to the start/end of the parameter range. + */ + const Span knots_tail = knots.take_back(order + degree); + for (const int64_t i : knots_tail.index_range().drop_back(1)) { const float head_span = knots[i + 1] - knots[i]; const float tail_span = knots_tail[i + 1] - knots_tail[i]; - if (abs(head_span - tail_span) > 0.0001f) { + if (abs(head_span - tail_span) > epsilon) { + return false; + } + } + return true; +} + +static bool detect_knot_mode_bezier(const int8_t degree, const Span multiplicity) +{ + const int8_t order = degree + 1; + if (multiplicity.first() != order || multiplicity.last() != order) { + return false; + } + + for (const int m : multiplicity.drop_front(1).drop_back(1)) { + if (m != degree) { + return false; + } + } + return true; +} + +static bool detect_knot_mode_uniform(const int8_t degree, + const Span knots, + const Span multiplicity, + bool clamped) +{ + constexpr float epsilon = 1e-7; + + /* Check if knot count matches multiplicity adjusted for clamped ends. For a uniform non-clamped + * curve, all multiplicity entries equals 1 and the array size should match. + */ + const int clamped_offset = clamped * degree; + if (knots.size() != multiplicity.size() - 2 * clamped_offset) { + return false; + } + + /* Ensure it's not a single segment with clamped ends (it would be a Bezier segment). */ + const Span unclamped_knots = knots.drop_front(clamped_offset).drop_back(clamped_offset); + if (!unclamped_knots.size()) { + return false; + } + + /* Verify spacing is uniform (excluding clamped ends). */ + const float uniform_delta = unclamped_knots[1] - unclamped_knots[0]; + for (const int64_t i : knots.index_range().drop_front(2)) { + if (abs((knots[i] - knots[i - 1]) - uniform_delta) < epsilon) { return false; } } @@ -159,61 +285,34 @@ short CurveFromGeometry::detect_knot_mode(const OBJImportParams &import_params, const int8_t degree, const Span indices, const Span knots, - const float2 range) + const Span multiplicity, + const float2 parameter_range) { short knot_mode = 0; if (import_params.close_spline_loops && indices.size() > degree) { - SET_FLAG_FROM_TEST(knot_mode, detect_knot_mode_cyclic(degree, indices, knots), CU_NURB_CYCLIC); + SET_FLAG_FROM_TEST( + knot_mode, detect_knot_mode_cyclic(degree, indices, knots, multiplicity), CU_NURB_CYCLIC); } - /* Figure out whether curve should have U endpoint flag set: - * the parameters should have at least (degree+1) values on each end, - * and their values should match curve range. */ - bool do_endpoints = false; - const int8_t order = degree + 1; - if (knots.size() >= order * 2) { - do_endpoints = true; - for (int i = 0; i < order; ++i) { - if (abs(knots[i] - range.x) > 0.0001f || abs(knots.last(i) - range.y) > 0.0001f) { - do_endpoints = false; - break; - } - } + if (detect_knot_mode_bezier(degree, multiplicity)) { + /* Currently endpoint flag is not parsed for Bezier, mainly because a clamped Bezier curve in + * Blender is either: + * a) A valid Bezier curve for given degree/order with correct number of points to form one. + * b) Not a valid Bezier curve for given degree, last span/segment is of a lower degree Bezier. + * + * Set ENDPOINT to true since legacy Bezier NURBS only validates and compute knots if it + * contains order + 1 control points unless endpoint is set...? + */ + SET_FLAG_FROM_TEST(knot_mode, true, CU_NURB_ENDPOINT); + SET_FLAG_FROM_TEST(knot_mode, true, CU_NURB_BEZIER); } - IndexRange inner_knots = knots.index_range(); - if (do_endpoints) { - knot_mode |= CU_NURB_ENDPOINT; - inner_knots = inner_knots.size() > 2 * degree ? - inner_knots.drop_front(degree).drop_back(degree) : - IndexRange(); - } - if (!inner_knots.is_empty()) { - const float first_step = knots[inner_knots.first() + 1] - knots[inner_knots.first()]; - bool is_spacing_equal = true; - bool is_bezier_knot = degree > 1; - int repeats = 0; - for (const int i : inner_knots.drop_front(1).drop_back(1)) { - const float step = knots[i + 1] - knots[i]; - if (abs(step - first_step) > 0.0001f) { - is_spacing_equal = false; - if (step == 0.0f) { - repeats++; - if (repeats > degree - 1) { - is_bezier_knot = false; - } - } - } - else if (repeats == degree - 1) { - repeats = 0; - } - else { - is_bezier_knot = false; - } - } - if (!is_spacing_equal) { - knot_mode |= is_bezier_knot ? CU_NURB_BEZIER : CU_NURB_CUSTOM; - } + else { + const bool clamped = detect_clamped_endpoint(degree, multiplicity); + SET_FLAG_FROM_TEST(knot_mode, clamped, CU_NURB_ENDPOINT); + SET_FLAG_FROM_TEST(knot_mode, + !detect_knot_mode_uniform(degree, knots, multiplicity, clamped), + CU_NURB_CUSTOM); } return knot_mode; } diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh index f8ba5b5a8a2..6f781920710 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh +++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.hh @@ -48,6 +48,7 @@ class CurveFromGeometry : NonMovable, NonCopyable { int8_t degree, Span indices, Span knots, + Span multiplicity, float2 range); }; } // namespace blender::io::obj diff --git a/tests/files/io_tests/obj/nurbs_manual.obj b/tests/files/io_tests/obj/nurbs_manual.obj index 8e0c99bfd7e..2044a05993a 100644 --- a/tests/files/io_tests/obj/nurbs_manual.obj +++ b/tests/files/io_tests/obj/nurbs_manual.obj @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83d3af4c9f93215fbd504c4ca6c3fec983c902a26c9078b95ee14565016642dd +oid sha256:8c66f333b6cbfd68bf1d3b3895f31c990d5feb2edf5c28d786ac0cfef7aa07ed size 1018 diff --git a/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt b/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt index 7bbbcf2ff7e..e4e78c9592b 100644 --- a/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt +++ b/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt @@ -57,13 +57,13 @@ ==== Curves: 7 - Curve 'NurbsCircle' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:8x1 order:4x4 cyclic:True,False endp:False,False - - (11.463, 0.000, -1.000) w:1.000 - - (10.463, 0.000, -1.000) w:1.000 - - (10.463, 0.000, 0.000) w:1.000 - ... - (12.463, 0.000, 1.000) w:1.000 - (12.463, 0.000, 0.000) w:1.000 - (12.463, 0.000, -1.000) w:1.000 + ... + - (10.463, 0.000, 0.000) w:1.000 + - (10.463, 0.000, 1.000) w:1.000 + - (11.463, 0.000, 1.000) w:1.000 - Curve 'NurbsCurve' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:False,False @@ -99,10 +99,10 @@ - Curve 'PolyCircle' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:2x2 cyclic:True,False endp:False,False + - (4.090, 0.000, -3.488) w:1.000 - (5.090, 0.000, -4.488) w:1.000 - (4.090, 0.000, -5.488) w:1.000 - (3.090, 0.000, -4.488) w:1.000 - - (4.090, 0.000, -3.488) w:1.000 - Curve 'PolyCurve' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:3x1 order:2x2 cyclic:False,False endp:False,False diff --git a/tests/files/io_tests/obj/reference/nurbs.txt b/tests/files/io_tests/obj/reference/nurbs.txt index e78126a913c..19c08bb7e8a 100644 --- a/tests/files/io_tests/obj/reference/nurbs.txt +++ b/tests/files/io_tests/obj/reference/nurbs.txt @@ -1,13 +1,13 @@ ==== Curves: 1 - Curve 'nurbs' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:9x1 order:4x4 cyclic:True,False endp:False,False - - (1.149, 0.964, -0.866) w:1.000 - - (1.149, -0.964, 0.866) w:1.000 - - (-1.500, -2.598, 0.000) w:1.000 - ... - (0.260, -1.477, -0.866) w:1.000 - (-1.410, -0.513, 0.866) w:1.000 - (-1.500, 2.598, 0.000) w:1.000 + ... + - (-1.410, 0.513, -0.866) w:1.000 + - (0.260, 1.477, 0.866) w:1.000 + - (3.000, 0.000, 0.000) w:1.000 ==== Objects: 1 - Obj 'nurbs' CURVE data:'nurbs' diff --git a/tests/files/io_tests/obj/reference/nurbs_curves.txt b/tests/files/io_tests/obj/reference/nurbs_curves.txt index 7bedaf1d4ba..bcf70936318 100644 --- a/tests/files/io_tests/obj/reference/nurbs_curves.txt +++ b/tests/files/io_tests/obj/reference/nurbs_curves.txt @@ -15,10 +15,10 @@ - Curve 'NurbsCurveCyclic' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:True,False endp:False,False - - (-6.000, -2.000, 0.000) w:1.000 - (-2.000, -2.000, 0.000) w:1.000 - (-2.000, 2.000, 0.000) w:1.000 - (-6.000, 2.000, 0.000) w:1.000 + - (-6.000, -2.000, 0.000) w:1.000 - Curve 'NurbsCurveDiffWeights' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:False,False diff --git a/tests/files/io_tests/obj/reference/nurbs_cyclic.txt b/tests/files/io_tests/obj/reference/nurbs_cyclic.txt index 998e12c8210..1515269d638 100644 --- a/tests/files/io_tests/obj/reference/nurbs_cyclic.txt +++ b/tests/files/io_tests/obj/reference/nurbs_cyclic.txt @@ -1,13 +1,13 @@ ==== Curves: 1 - Curve 'nurbs_cyclic' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:28x1 order:4x4 cyclic:True,False endp:False,False - - (0.935, 0.000, 3.518) w:1.000 - - (-0.050, 0.000, 2.052) w:1.000 - - (1.001, 0.000, 1.462) w:1.000 - ... - (2.591, 0.000, -0.795) w:1.000 - (4.054, 0.000, 0.651) w:1.000 - (3.281, 0.000, 3.043) w:1.000 + ... + - (4.054, 0.000, -0.651) w:1.000 + - (2.591, 0.000, 0.795) w:1.000 + - (1.500, 0.000, 0.000) w:1.000 ==== Objects: 1 - Obj 'nurbs_cyclic' CURVE data:'nurbs_cyclic' diff --git a/tests/files/io_tests/obj/reference/nurbs_manual.txt b/tests/files/io_tests/obj/reference/nurbs_manual.txt index c8f8123e2a9..cdf25ba17af 100644 --- a/tests/files/io_tests/obj/reference/nurbs_manual.txt +++ b/tests/files/io_tests/obj/reference/nurbs_manual.txt @@ -1,10 +1,10 @@ ==== Curves: 4 - Curve 'Curve_Cyclic' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:True,False endp:False,False - - (-2.000, 0.000, -2.000) w:1.000 - (-2.000, 0.000, 2.000) w:1.000 - (2.000, 0.000, 2.000) w:1.000 - (2.000, 0.000, -2.000) w:1.000 + - (-2.000, 0.000, -2.000) w:1.000 - Curve 'Curve_Endpoints' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:5x1 order:4x4 cyclic:False,False endp:True,False @@ -12,7 +12,7 @@ - (2.000, 0.000, 2.000) w:1.000 - (2.000, 0.000, -2.000) w:1.000 - (-2.000, 0.000, -2.000) w:1.000 - - (-2.000, 0.000, 2.000) w:1.000 + - (2.000, 0.000, 2.000) w:1.000 - Curve 'Curve_NonUniform_Parm' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:5x1 order:4x4 cyclic:False,False endp:False,False