Fix: Improve OBJ NURBS IO, support exporting custom knots

Corrects behavior with NURBS knot values in .obj exporter. Knot values
denoting the curve parameter range and values at the boundary region
in the span ends had hardcoded knot values. It also implemented its own
knot calculation, which is not ideal...

Importer is updated to not try to second guess the knot values.
Not entirely sure what it was trying to do but it used wrong indices
and missed writing the end of the knot vector. Combined the changes
should make it possible to import and export a simple NURBS curve with
custom knots and leaving it intact.

This replaces some of the erronous behavior using functions from [new]
Curves implementation. Mixing new and legacy curve implementation is not
ideal but exporter is exporting POLY curves as NURBS while legacy method
does not support computing the knot vector. To avoid introducing
additional branch cases nor update legacy functions, using the new
functions seems to be the correct choice. These functions should be
functionally equivalent but is not identical (e.g. legacy curve returns
knots in [0, 1] range). It should also make it easier to transition to
exporting new Curves.

Pull Request: https://projects.blender.org/blender/blender/pulls/138732
This commit is contained in:
Mattias Fredriksson
2025-05-12 16:51:21 +02:00
committed by Hans Goudey
parent bf325bc1b8
commit d0cf7dd8b5
11 changed files with 100 additions and 64 deletions

View File

@@ -8,9 +8,10 @@
* \ingroup bke
*/
struct ListBase;
#include "DNA_curves_types.h"
struct Curve;
struct Curves;
struct ListBase;
namespace blender::bke {
@@ -24,4 +25,9 @@ Curves *curve_legacy_to_curves(const Curve &curve_legacy);
*/
Curves *curve_legacy_to_curves(const Curve &curve_legacy, const ListBase &nurbs_list);
/**
* Determine Curves knot mode from legacy flag.
*/
KnotsMode knots_mode_from_legacy(short flag);
} // namespace blender::bke

View File

@@ -64,7 +64,7 @@ static NormalMode normal_mode_from_legacy(const short twist_mode)
return NORMAL_MODE_MINIMUM_TWIST;
}
static KnotsMode knots_mode_from_legacy(const short flag)
KnotsMode knots_mode_from_legacy(const short flag)
{
if (flag & CU_NURB_CUSTOM) {
return NURBS_KNOT_MODE_CUSTOM;

View File

@@ -466,8 +466,13 @@ void OBJWriter::write_edges_indices(FormatHandler &fh,
void OBJWriter::write_nurbs_curve(FormatHandler &fh, const OBJCurve &obj_nurbs_data) const
{
const int total_splines = obj_nurbs_data.total_splines();
for (int spline_idx = 0; spline_idx < total_splines; spline_idx++) {
/* Double check no surface is passed in as they are no supported (this is filtered when parsed)
*/
BLI_assert(obj_nurbs_data.get_spline(spline_idx)->pntsv == 1);
const int total_vertices = obj_nurbs_data.total_spline_vertices(spline_idx);
for (int vertex_idx = 0; vertex_idx < total_vertices; vertex_idx++) {
const float3 vertex_coords = obj_nurbs_data.vertex_coordinates(
@@ -476,10 +481,10 @@ void OBJWriter::write_nurbs_curve(FormatHandler &fh, const OBJCurve &obj_nurbs_d
}
const char *nurbs_name = obj_nurbs_data.get_curve_name();
const int nurbs_degree = obj_nurbs_data.get_nurbs_degree(spline_idx);
const int degree_u = obj_nurbs_data.get_nurbs_degree_u(spline_idx);
fh.write_obj_group(nurbs_name);
fh.write_obj_cstype();
fh.write_obj_nurbs_degree(nurbs_degree);
fh.write_obj_nurbs_degree(degree_u);
/**
* The numbers written here are indices into the vertex coordinates written
* earlier, relative to the line that is going to be written.
@@ -487,36 +492,24 @@ void OBJWriter::write_nurbs_curve(FormatHandler &fh, const OBJCurve &obj_nurbs_d
* 0.0 1.0 -1 -2 -3 -4 for a non-cyclic curve with 4 vertices.
* 0.0 1.0 -1 -2 -3 -4 -1 -2 -3 for a cyclic curve with 4 vertices.
*/
const int total_control_points = obj_nurbs_data.total_spline_control_points(spline_idx);
const int num_points_u = obj_nurbs_data.num_control_points_u(spline_idx);
Vector<float> knot_buffer;
Span<float> knotsu = obj_nurbs_data.get_knots_u(spline_idx, knot_buffer);
fh.write_obj_curve_begin();
for (int i = 0; i < total_control_points; i++) {
fh.write_obj_nurbs_parm(knotsu[degree_u]);
fh.write_obj_nurbs_parm(knotsu.last(degree_u));
for (int i = 0; i < num_points_u; i++) {
/* "+1" to keep indices one-based, even if they're negative: i.e., -1 refers to the
* last vertex coordinate, -2 second last. */
fh.write_obj_face_v(-((i % total_vertices) + 1));
}
fh.write_obj_curve_end();
/**
* In `parm u 0 0.1 ..` line:, (total control points + 2) equidistant numbers in the
* parameter range are inserted. However for curves with endpoint flag,
* first degree+1 numbers are zeroes, and last degree+1 numbers are ones
*/
const short flagsu = obj_nurbs_data.get_nurbs_flagu(spline_idx);
const bool cyclic = flagsu & CU_NURB_CYCLIC;
const bool endpoint = !cyclic && (flagsu & CU_NURB_ENDPOINT);
fh.write_obj_nurbs_parm_begin();
for (int i = 1; i <= total_control_points + 2; i++) {
float parm = 1.0f * i / (total_control_points + 2 + 1);
if (endpoint) {
if (i <= nurbs_degree) {
parm = 0;
}
else if (i > total_control_points + 2 - nurbs_degree) {
parm = 1;
}
}
fh.write_obj_nurbs_parm(parm);
for (const float &u : knotsu) {
fh.write_obj_nurbs_parm(u);
}
fh.write_obj_nurbs_parm_end();
fh.write_obj_nurbs_group_end();

View File

@@ -140,7 +140,7 @@ class FormatHandler : NonCopyable, NonMovable {
}
void write_obj_curve_begin()
{
write_impl("curv 0.0 1.0");
write_impl("curv");
}
void write_obj_curve_end()
{
@@ -148,7 +148,7 @@ class FormatHandler : NonCopyable, NonMovable {
}
void write_obj_nurbs_parm_begin()
{
write_impl("parm u 0.0");
write_impl("parm u");
}
void write_obj_nurbs_parm(float v)
{
@@ -156,7 +156,7 @@ class FormatHandler : NonCopyable, NonMovable {
}
void write_obj_nurbs_parm_end()
{
write_impl(" 1.0\n");
write_impl("\n");
}
void write_obj_nurbs_group_end()
{

View File

@@ -12,6 +12,9 @@
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BKE_curve_legacy_convert.hh"
#include "BKE_curves.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"
@@ -52,6 +55,11 @@ int OBJCurve::total_splines() const
return BLI_listbase_count(&export_curve_->nurb);
}
const Nurb *OBJCurve::get_spline(const int spline_index) const
{
return static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
}
int OBJCurve::total_spline_vertices(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
@@ -71,29 +79,53 @@ float3 OBJCurve::vertex_coordinates(const int spline_index,
return r_coord;
}
int OBJCurve::total_spline_control_points(const int spline_index) const
int OBJCurve::num_control_points_u(int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
int degree = nurb->type == CU_POLY ? 1 : nurb->orderu - 1;
/* Total control points = Number of points in the curve (+ degree of the
* curve if it is cyclic). */
int tot_control_points = nurb->pntsv * nurb->pntsu;
if (nurb->flagu & CU_NURB_CYCLIC) {
tot_control_points += degree;
}
return tot_control_points;
return nurb->pntsu + (nurb->flagu & CU_NURB_CYCLIC ? get_nurbs_degree_u(spline_index) : 0);
}
int OBJCurve::get_nurbs_degree(const int spline_index) const
int OBJCurve::num_control_points_v(int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
return nurb->pntsv + (nurb->flagv & CU_NURB_CYCLIC ? get_nurbs_degree_v(spline_index) : 0);
}
int OBJCurve::get_nurbs_degree_u(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
return nurb->type == CU_POLY ? 1 : nurb->orderu - 1;
}
int OBJCurve::get_nurbs_degree_v(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
return nurb->type == CU_POLY ? 1 : nurb->orderv - 1;
}
short OBJCurve::get_nurbs_flagu(const int spline_index) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
return nurb->flagu;
}
Span<float> OBJCurve::get_knots_u(int spline_index, Vector<float> &knot_buffer) const
{
const Nurb *const nurb = static_cast<Nurb *>(BLI_findlink(&export_curve_->nurb, spline_index));
const short flag = nurb->flagu;
const int8_t order = get_nurbs_degree_u(spline_index) + 1; /* Use utility in case of POLY */
const bool cyclic = flag & CU_NURB_CYCLIC;
const int knot_count = bke::curves::nurbs::knots_num(nurb->pntsu, order, cyclic);
if (flag & CU_NURB_CUSTOM) {
return Span<float>(nurb->knotsu, knot_count);
}
knot_buffer.resize(knot_count);
bke::curves::nurbs::calculate_knots(
nurb->pntsu, bke::knots_mode_from_legacy(flag), order, cyclic, knot_buffer);
return knot_buffer;
}
} // namespace blender::io::obj

View File

@@ -36,6 +36,7 @@ class OBJCurve : NonCopyable {
const char *get_curve_name() const;
int total_splines() const;
const Nurb *get_spline(int spline_index) const;
/**
* \param spline_index: Zero-based index of spline of interest.
* \return Total vertices in a spline.
@@ -46,18 +47,29 @@ class OBJCurve : NonCopyable {
*/
float3 vertex_coordinates(int spline_index, int vertex_index, float global_scale) const;
/**
* Get total control points of the NURBS spline at the given index. This is different than total
* vertices of a spline.
* Get the number of control points on the U-dimension.
*/
int total_spline_control_points(int spline_index) const;
int num_control_points_u(int spline_index) const;
/**
* Get the degree of the NURBS spline at the given index.
* Get the number of control points on the V-dimension.
*/
int get_nurbs_degree(int spline_index) const;
int num_control_points_v(int spline_index) const;
/**
* Get the degree of the NURBS spline for the U-dimension.
*/
int get_nurbs_degree_u(int spline_index) const;
/**
* Get the degree of the NURBS spline for the V-dimension.
*/
int get_nurbs_degree_v(int spline_index) const;
/**
* Get the U flags (CU_NURB_*) of the NURBS spline at the given index.
*/
short get_nurbs_flagu(int spline_index) const;
/**
* Get the knot vector for the U-dimension. Computes knots using the buffer if necessary.
*/
Span<float> get_knots_u(int spline_index, Vector<float> &buffer) const;
private:
/**

View File

@@ -9,6 +9,7 @@
#include "BKE_lib_id.hh"
#include "BKE_object.hh"
#include "BLI_array_utils.hh"
#include "BLI_listbase.h"
#include "BLI_math_vector.h"
@@ -126,16 +127,7 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import
if (nurb->flagu & CU_NURB_CUSTOM) {
BKE_nurb_knot_alloc_u(nurb);
Span<float> knots = nurbs_geometry.parm.as_span();
if (nurb->flagu & CU_NURB_CYCLIC) {
knots = knots.drop_front(degree);
const float last_real_knot = knots[nurb->pntsu + degree];
MutableSpan<float> virtual_knots(nurb->knotsu + degree + 1, degree);
for (const int i : IndexRange(degree)) {
virtual_knots[i] = last_real_knot + knots[degree + 1 + i] - knots[degree];
}
}
std::copy_n(knots.data(), knots.size(), nurb->knotsu);
array_utils::copy<float>(nurbs_geometry.parm, MutableSpan<float>{nurb->knotsu, KNOTSU(nurb)});
}
else {
BKE_nurb_knot_calc_u(nurb);

View File

@@ -216,10 +216,11 @@ TEST(obj_exporter_writer, format_handler_buffer_chunking)
h.write_obj_curve_begin();
h.write_obj_newline();
h.write_obj_nurbs_parm_begin();
h.write_obj_nurbs_parm(0.0f);
h.write_obj_newline();
size_t got_blocks = h.get_block_count();
ASSERT_EQ(got_blocks, 7);
ASSERT_EQ(got_blocks, 6);
std::string got_string = h.get_as_string();
using namespace std::string_literals;
@@ -229,8 +230,8 @@ o abcde
o abcdef
o 012345678901234567890123456789abcd
o 123
curv 0.0 1.0
parm u 0.0
curv
parm u 0.000000
)";
ASSERT_EQ(got_string, expected);
}

Binary file not shown.

BIN
tests/files/io_tests/obj/nurbs.obj (Stored with Git LFS)

Binary file not shown.

BIN
tests/files/io_tests/obj/nurbs_curves.obj (Stored with Git LFS)

Binary file not shown.