USD: Import UsdNurbsCurves as Curves instead of old Curve

Refactor and revamp import and export of `UsdGeomNurbsCurves` prim
objects.

Fixes #130056, among other things.

Summary of changes and enhancements:
- Export:
  - Write out `nurb_weight` attribute as the USD `pointWeights` primvar
  - Properly write out cyclic NURBS curves data (* see notes)
- Import:
  - Import using the new `Curves` datablock rather than the old `Curve`
  - Properly read in cyclic NURBS curves data (* see notes)
  - Tries harder to match incoming knot vector to standard `knots_mode`,
    will use Custom otherwise
  - Support import of all custom primvars and data attached to the prim
    (for use with Geometry Nodes etc.) (* see notes)

Tests were added which check a variety of point count, order, knot_mode,
and cyclic combinations (generated through Geometry Nodes). A small
number of hand-crafted curves were used to test the Custom knots_mode
support on import. Additionally, the tests cover the case when there are
multiple curves defined for a single object.

Notes:
- Cyclic NURBS support is reliant on the current, under-spec'd, USD
  documentation. Changes may be required in the future if/when the USD
  spec is clarified: https://github.com/PixarAnimationStudios/OpenUSD/issues/3740
- Some Cyclic x knots_mode combinations are not correct and would
  require more research to determine how to properly address.
- Custom attributes are not imported for Cyclic NURBS curves yet. Those
  will require additional work to function correctly and are also
  reliant on seeing how the USD spec changes.

Pull Request: https://projects.blender.org/blender/blender/pulls/143970
This commit is contained in:
Jesse Yurkovich
2025-08-27 19:34:46 +02:00
committed by Jesse Yurkovich
parent c61d958872
commit 7111e95527
14 changed files with 5179 additions and 216 deletions

View File

@@ -895,9 +895,10 @@ class USDExportTest(AbstractUSDTest):
self.assertEqual(self.round_vector(usd_extent[0]), extent[0])
self.assertEqual(self.round_vector(usd_extent[1]), extent[1])
def check_nurbs_curve(prim, cyclic, orders, vert_counts, knots_count, extent):
def check_nurbs_curve(prim, cyclic, orders, vert_counts, weights, knots_count, extent):
self.assertEqual(prim.GetOrderAttr().Get(), orders)
self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts)
self.assertEqual(self.round_vector(prim.GetPointWeightsAttr().Get()), weights)
self.assertEqual(prim.GetWidthsInterpolation(), "vertex")
knots = prim.GetKnotsAttr().Get()
usd_extent = prim.GetExtentAttr().Get()
@@ -936,11 +937,14 @@ 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.6891, -1.0117], [3.0896, 1.9583, 1.0293]])
weights = [1] * 12
check_nurbs_curve(
curve, False, [4, 4], [6, 6], weights, 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"))
check_nurbs_curve(curve, True, [3], [8], 13, [[-2.0, -2.0, -1.0], [2.0, 2.0, 1.0]])
weights = self.round_vector([1, math.sqrt(2) / 2] * 5)
check_nurbs_curve(curve, True, [3], [10], weights, 13, [[-2, -2, -1], [2, 2, 1]])
def test_export_animation(self):
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend"))

View File

@@ -2018,9 +2018,23 @@ class USDImportComparisonTest(unittest.TestCase):
from modules import io_report
report = io_report.Report("USD Import", self.output_dir, comparisondir, comparisondir.joinpath("reference"))
io_report.Report.context_lines = 8
bpy.utils.register_class(CompareTestSupportHook)
for input_file in input_files:
with self.subTest(pathlib.Path(input_file).stem):
input_file_path = pathlib.Path(input_file)
io_report.Report.side_to_print_single_line = 5
io_report.Report.side_to_print_multi_line = 3
CompareTestSupportHook.reset_config()
if input_file_path.name in ("nurbs-gen-single.usda", "nurbs-gen-multiple.usda", "nurbs-custom.usda"):
CompareTestSupportHook.do_curve_rename = True
io_report.Report.side_to_print_single_line = 10
io_report.Report.side_to_print_multi_line = 10
with self.subTest(input_file_path.stem):
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
ok = report.import_and_check(
input_file, lambda filepath, params: bpy.ops.wm.usd_import(
@@ -2028,9 +2042,32 @@ class USDImportComparisonTest(unittest.TestCase):
if not ok:
self.fail(f"{input_file.stem} import result does not match expectations")
bpy.utils.unregister_class(CompareTestSupportHook)
report.finish("io_usd_import")
class CompareTestSupportHook(bpy.types.USDHook):
bl_idname = "CompareTestSupportHook"
bl_label = "Support some Comparison "
do_curve_rename = False
@staticmethod
def reset_config():
CompareTestSupportHook.do_curve_rename = False
@staticmethod
def on_import(context):
prim_map = context.get_prim_map()
if CompareTestSupportHook.do_curve_rename:
for prim_path, objects in prim_map.items():
if isinstance(objects[0], bpy.types.Object):
objects[0].name = prim_path.name
elif isinstance(objects[0], bpy.types.Curves):
objects[0].name = prim_path.GetParentPath().name
class GetPrimMapUsdImportHook(bpy.types.USDHook):
bl_idname = "get_prim_map_usd_import_hook"
bl_label = "Get Prim Map Usd Import Hook"