Curves: Enhance tesselation of NURBS with corners

Current NURBS evaluation handles corners or sharp angles poorly. Sharp
edges appear when a knot vector value is repeated `order - 1` times.
Users can make sharp corners by creating NURBS curve with `Bezier` knot
mode or by setting `order` to 2 for legacy curves. The problem occurs
because current algorithm takes all the curve's definition interval,
divides it into equal parts and evaluates at those points, but corners
are exactly on repeated knot's. To hit those, the resolution has to be
increased higher than required for the rest of the curve.

The new algorithm divides non zero length intervals between two adjacent
knots into equal parts. This way corners are hit with a resolution of 1.
This does change the evaluated points of NURBS curves, which is why some
test results have to be updated in this commit.

Pull Request: https://projects.blender.org/blender/blender/pulls/138565
This commit is contained in:
Laurynas Duburas 2025-06-12 16:22:21 +02:00 committed by Hans Goudey
parent e9bb58e6df
commit 3c407ebeaa
17 changed files with 147 additions and 69 deletions

View File

@ -854,8 +854,12 @@ bool check_valid_num_and_order(int points_num, int8_t order, bool cyclic, KnotsM
* for predictability and so that cached basis weights of NURBS curves with these properties can be
* shared.
*/
int calculate_evaluated_num(
int points_num, int8_t order, bool cyclic, int resolution, KnotsMode knots_mode);
int calculate_evaluated_num(int points_num,
int8_t order,
bool cyclic,
int resolution,
KnotsMode knots_mode,
Span<float> knots);
/**
* Calculate the length of the knot vector for a NURBS curve with the given properties.
@ -906,6 +910,7 @@ Vector<int> calculate_multiplicity_sequence(Span<float> knots);
void calculate_basis_cache(int points_num,
int evaluated_num,
int8_t order,
int resolution,
bool cyclic,
Span<float> knots,
BasisCache &basis_cache);

View File

@ -32,16 +32,57 @@ bool check_valid_num_and_order(const int points_num,
return true;
}
static int calc_nonzero_knot_spans(const int points_num,
const KnotsMode mode,
const int8_t order,
const bool cyclic)
{
const bool is_bezier = ELEM(mode, NURBS_KNOT_MODE_BEZIER, NURBS_KNOT_MODE_ENDPOINT_BEZIER);
const bool is_end_point = ELEM(mode, NURBS_KNOT_MODE_ENDPOINT, NURBS_KNOT_MODE_ENDPOINT_BEZIER);
/* Inner knots are always repeated once except on Bezier case. */
const int repeat_inner = is_bezier ? order - 1 : 1;
/* For non endpoint Bezier repeated knots are shifted by one. */
const int knots_before_geometry = order + int(is_bezier && !is_end_point && order > 2);
const int knots_after_geometry = order - 1 +
(cyclic && mode == NURBS_KNOT_MODE_ENDPOINT ? order - 2 : 0);
const int knots_total = knots_num(points_num, order, cyclic);
/* On these knots as parameters actual geometry is generated. */
const int geometry_knots = knots_total - knots_before_geometry - knots_after_geometry;
/* `repeat_inner - 1` is added to `ceil`. */
const int non_zero_knots = (geometry_knots + repeat_inner - 1) / repeat_inner;
return non_zero_knots;
}
static int count_nonzero_knot_spans(const int points_num,
const int order,
const bool cyclic,
const Span<float> knots)
{
BLI_assert(points_num > 0);
const int degree = order - 1;
int span_num = 0;
for (const int knot_span : IndexRange::from_begin_end(cyclic ? 0 : degree, points_num)) {
span_num += (knots[knot_span + 1] - knots[knot_span]) > 0.0f;
}
return span_num;
}
int calculate_evaluated_num(const int points_num,
const int8_t order,
const bool cyclic,
const int resolution,
const KnotsMode knots_mode)
const KnotsMode knots_mode,
const Span<float> knots)
{
if (!check_valid_num_and_order(points_num, order, cyclic, knots_mode)) {
return points_num;
}
return resolution * segments_num(points_num, cyclic);
const int nonzero_span_num = knots_mode == KnotsMode::NURBS_KNOT_MODE_CUSTOM &&
!knots.is_empty() ?
count_nonzero_knot_spans(points_num, order, cyclic, knots) :
calc_nonzero_knot_spans(points_num, knots_mode, order, cyclic);
return resolution * nonzero_span_num + int(!cyclic);
}
int knots_num(const int points_num, const int8_t order, const bool cyclic)
@ -206,6 +247,7 @@ static void calculate_basis_for_point(const float parameter,
void calculate_basis_cache(const int points_num,
const int evaluated_num,
const int8_t order,
const int resolution,
const bool cyclic,
const Span<float> knots,
BasisCache &basis_cache)
@ -225,19 +267,34 @@ void calculate_basis_cache(const int points_num,
MutableSpan<int> basis_start_indices(basis_cache.start_indices);
const int last_control_point_index = cyclic ? points_num + degree : points_num;
const int evaluated_segment_num = segments_num(evaluated_num, cyclic);
const float start = knots[degree];
const float end = knots[last_control_point_index];
const float step = (end - start) / evaluated_segment_num;
for (const int i : IndexRange(evaluated_num)) {
/* Clamp parameter due to floating point inaccuracy. */
const float parameter = std::clamp(start + step * i, knots[0], knots[points_num + degree]);
int eval_point = 0;
MutableSpan<float> point_weights = basis_weights.slice(i * order, order);
calculate_basis_for_point(
parameter, last_control_point_index, degree, knots, point_weights, basis_start_indices[i]);
for (const int knot_span : IndexRange::from_begin_end(degree, last_control_point_index)) {
const float start = knots[knot_span];
const float end = knots[knot_span + 1];
if (start == end) {
continue;
}
const float step_width = (end - start) / resolution;
for (const int step : IndexRange::from_begin_size(0, resolution)) {
const float parameter = start + step * step_width;
calculate_basis_for_point(parameter,
last_control_point_index,
degree,
knots,
basis_weights.slice(eval_point * order, order),
basis_start_indices[eval_point]);
eval_point++;
}
}
if (!cyclic) {
calculate_basis_for_point(knots[last_control_point_index],
last_control_point_index,
degree,
knots,
basis_weights.slice(eval_point * order, order),
basis_start_indices[eval_point]);
}
}

View File

@ -501,7 +501,10 @@ static void build_mesh_positions(const CurvesInfo &curves_info,
const bool ignore_profile_position = profile_positions.size() == 1 &&
math::is_equal(profile_positions.first(), float3(0.0f));
if (ignore_profile_position) {
if (mesh.verts_num == curves_info.main.points_num()) {
if (mesh.verts_num == curves_info.main.points_num() &&
/* NURBS can have equal evaluated and positions sizes, but different coords. */
!curves_info.main.has_curve_with_type(CURVE_TYPE_NURBS))
{
const GAttributeReader src = curves_info.main.attributes().lookup("position");
if (src.sharing_info && src.varray.is_span()) {
const AttributeInitShared init(src.varray.get_internal_span().data(), *src.sharing_info);

View File

@ -657,6 +657,8 @@ static void calculate_evaluated_offsets(const CurvesGeometry &curves,
const VArray<int8_t> nurbs_orders = curves.nurbs_orders();
const VArray<int8_t> nurbs_knots_modes = curves.nurbs_knots_modes();
const OffsetIndices<int> custom_knots_by_curve = curves.nurbs_custom_knots_by_curve();
const Span<float> all_custom_knots = curves.nurbs_custom_knots();
build_offsets(offsets, [&](const int curve_index) -> int {
const IndexRange points = points_by_curve[curve_index];
@ -676,11 +678,17 @@ static void calculate_evaluated_offsets(const CurvesGeometry &curves,
return all_bezier_offsets[offsets.last()];
}
case CURVE_TYPE_NURBS:
return curves::nurbs::calculate_evaluated_num(points.size(),
nurbs_orders[curve_index],
cyclic[curve_index],
resolution[curve_index],
KnotsMode(nurbs_knots_modes[curve_index]));
const bool is_cyclic = cyclic[curve_index];
const int8_t order = nurbs_orders[curve_index];
const KnotsMode knots_mode = KnotsMode(nurbs_knots_modes[curve_index]);
const IndexRange custom_knots_range = custom_knots_by_curve[curve_index];
const Span<float> custom_knots = knots_mode == NURBS_KNOT_MODE_CUSTOM &&
!all_custom_knots.is_empty() &&
!custom_knots_range.is_empty() ?
all_custom_knots.slice(custom_knots_range) :
Span<float>();
return curves::nurbs::calculate_evaluated_num(
points.size(), order, is_cyclic, resolution[curve_index], knots_mode, custom_knots);
}
BLI_assert_unreachable();
return 0;
@ -755,6 +763,7 @@ void CurvesGeometry::ensure_nurbs_basis_cache() const
const OffsetIndices<int> custom_knots_by_curve = this->nurbs_custom_knots_by_curve();
const VArray<bool> cyclic = this->cyclic();
const VArray<int8_t> orders = this->nurbs_orders();
const VArray<int> resolutions = this->resolution();
const VArray<int8_t> knots_modes = this->nurbs_knots_modes();
const Span<float> custom_knots = this->nurbs_custom_knots();
@ -765,6 +774,7 @@ void CurvesGeometry::ensure_nurbs_basis_cache() const
const IndexRange evaluated_points = evaluated_points_by_curve[curve_index];
const int8_t order = orders[curve_index];
const int resolution = resolutions[curve_index];
const bool is_cyclic = cyclic[curve_index];
const KnotsMode mode = KnotsMode(knots_modes[curve_index]);
@ -782,8 +792,13 @@ void CurvesGeometry::ensure_nurbs_basis_cache() const
custom_knots,
knots);
curves::nurbs::calculate_basis_cache(
points.size(), evaluated_points.size(), order, is_cyclic, knots, r_data[curve_index]);
curves::nurbs::calculate_basis_cache(points.size(),
evaluated_points.size(),
order,
resolution,
is_cyclic,
knots,
r_data[curve_index]);
}
});
});

View File

@ -329,16 +329,17 @@ TEST(curves_geometry, NURBSEvaluation)
Span<float3> evaluated_positions = curves.evaluated_positions();
static const Array<float3> result_1{{
{0.166667, 0.833333, 0}, {0.150006, 0.815511, 0}, {0.134453, 0.796582, 0},
{0.119924, 0.776627, 0}, {0.106339, 0.75573, 0}, {0.0936146, 0.733972, 0},
{0.0816693, 0.711434, 0}, {0.0704211, 0.6882, 0}, {0.0597879, 0.66435, 0},
{0.0496877, 0.639968, 0}, {0.0400385, 0.615134, 0}, {0.0307584, 0.589931, 0},
{0.0217653, 0.564442, 0}, {0.0129772, 0.538747, 0}, {0.00431208, 0.512929, 0},
{-0.00431208, 0.487071, 0}, {-0.0129772, 0.461253, 0}, {-0.0217653, 0.435558, 0},
{-0.0307584, 0.410069, 0}, {-0.0400385, 0.384866, 0}, {-0.0496877, 0.360032, 0},
{-0.0597878, 0.33565, 0}, {-0.0704211, 0.3118, 0}, {-0.0816693, 0.288566, 0},
{-0.0936146, 0.266028, 0}, {-0.106339, 0.24427, 0}, {-0.119924, 0.223373, 0},
{-0.134453, 0.203418, 0}, {-0.150006, 0.184489, 0}, {-0.166667, 0.166667, 0},
{0.166667, 0.833333, 0},
{0.121333, 0.778667, 0},
{0.084, 0.716, 0},
{0.0526667, 0.647333, 0},
{0.0253333, 0.574667, 0},
{0, 0.5, 0},
{-0.0253333, 0.425333, 0},
{-0.0526667, 0.352667, 0},
{-0.084, 0.284, 0},
{-0.121333, 0.221333, 0},
{-0.166667, 0.166667, 0},
}};
for (const int i : evaluated_positions.index_range()) {
EXPECT_V3_NEAR(evaluated_positions[i], result_1[i], 1e-5f);

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b265d844db7a6f84520e6c01987eec91eab20bd4eeb3e061e83e6b4a35d4323
size 8652
oid sha256:550fa013ce1c048e889087fcbda18c8043456cca3759375064e1b4c8111308be
size 7531

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72b89d5470bb42af366946d1d32cc653001facae1266ea6342f1609a190e55a9
oid sha256:c2098e5dce32abb74d49e49f71479ecdd7a9acfe4f86b699e36584c4103fc4ba
size 6709

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d11c9cb2b0299b3d425864f1f583a7de2f4152306d7ed886cdcffb829bf93a8
oid sha256:f3cc276cb772ecef16c332de06fce6c5bfc577fabdea07a8d3b6293f8727bdfe
size 321069

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be872ec81f8cbb50121b40c5de90cb3ca7e19ff1eb1421ba8ad27fa4681473f6
oid sha256:c3a4e24475815b526f6fc180c9d0016a482434e822a3270846ecda9e103a60c7
size 321239

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7da3b2cb989d0fe71a95a4e32b302b538a7baf406d2728d7b5d2265c49cd8c04
oid sha256:98389651f47f3d6b2f5eca34716d8151a7c6a033e379a9cdc03219514fe034f8
size 4213

View File

@ -65,37 +65,34 @@
- (11.193, 0.000, 0.969)
- (10.989, 0.000, 0.883)
- Mesh 'NurbsCurve' vtx:9 face:0 loop:0 edge:8
- 0/1 1/2 2/3 3/4 4/5 5/6 6/7 7/8
- Mesh 'NurbsCurve' vtx:4 face:0 loop:0 edge:3
- 0/1 1/2 2/3
- attr 'position' FLOAT_VECTOR POINT
- (7.730, 0.000, -0.375)
- (7.886, 0.000, -0.581)
- (8.063, 0.000, -0.734)
...
- (8.846, 0.000, -0.920)
- (9.030, 0.000, -0.887)
- (8.189, 0.000, -0.809)
- (8.717, 0.000, -0.927)
- (9.197, 0.000, -0.833)
- Mesh 'NurbsCurve2' vtx:24 face:0 loop:0 edge:23
- 0/1 1/2 2/3 3/4 4/5 ... 18/19 19/20 20/21 21/22 22/23
- Mesh 'NurbsCurve2' vtx:17 face:0 loop:0 edge:16
- 0/1 1/2 2/3 3/4 4/5 ... 11/12 12/13 13/14 14/15 15/16
- attr 'position' FLOAT_VECTOR POINT
- (5.857, 0.000, 5.211)
- (5.883, 0.000, 4.843)
- (5.990, 0.000, 4.491)
- (5.921, 0.000, 4.685)
- (6.135, 0.000, 4.218)
...
- (9.419, 0.000, 3.357)
- (9.557, 0.000, 3.570)
- (9.283, 0.000, 3.248)
- (9.500, 0.000, 3.465)
- (9.665, 0.000, 3.871)
- Mesh 'NurbsPathCurve' vtx:31 face:0 loop:0 edge:29
- 0/1 1/2 2/3 3/4 4/5 ... 25/26 26/27 27/28 28/29 29/30
- Mesh 'NurbsPathCurve' vtx:15 face:0 loop:0 edge:13
- 0/1 1/2 2/3 3/4 4/5 ... 9/10 10/11 11/12 12/13 13/14
- attr 'position' FLOAT_VECTOR POINT
- (13.691, 0.000, 0.000)
- (14.059, 0.000, 0.419)
- (14.383, 0.000, 0.670)
- (14.345, 0.000, 0.646)
- (14.865, 0.000, 0.823)
...
- (16.242, 0.000, -0.671)
- (16.339, 0.000, -0.583)
- (15.852, 0.000, -0.877)
- (16.160, 0.000, -0.732)
- (16.430, 0.000, -0.479)
- Mesh 'PolyCircle' vtx:4 face:0 loop:0 edge:4

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5a8eda24804f2c3ac01e93af91af20b45ccbf57fee6bf16b81abc47ce781eb99
size 145664
oid sha256:6393161f668410636448320997eabecaff0d2174bcbec6e2b580cac62487b8f1
size 1603172

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19a9d325f95220e8ddc3a787a02d18a78480a6a43ad7177d128f069bc6163dbf
size 150542
oid sha256:fc4ccfecbac1c16487705e4d62137f40874df01f1606a2912d25d01f38fba768
size 991954

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81af832bacc4e62ad28785a998d1b3a09b555058466553481ce30434b041d402
size 89539
oid sha256:14b15a0a23f21cfefa67b3b437ae68615464a1275c354ca9b6c92b98f388d584
size 642331

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6346756e7672b3330f0564518c98419d2616ab5d221e0afa4066397e733db118
size 135396
oid sha256:8bdd3fb883c784f7dfe59ab11315596f577f537cb429ca23a7229f1ef5bd0cbb
size 1014822

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4ed3a58f5c685a7dbae64b7cfaee3170b90eced80ea77061ef790f5e9c7b94dd
size 124628
oid sha256:5bca0f399a4a8eab1d3e9ace766c1acc55f8b1b5c542020a84b7cd3db5876f12
size 1060327

View File

@ -936,7 +936,7 @@ class USDExportTest(AbstractUSDTest):
# Contains 2 NURBS curves
curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCurve/NurbsCurve"))
check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-1.75, -2.6898, -1.0117], [3.0896, 1.9583, 1.0293]])
check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-1.75, -2.6891, -1.0117], [3.0896, 1.9583, 1.0293]])
# Contains 1 NURBS curve
curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCircle/NurbsCircle"))