From e19f9b32a87f353d47dc247fda225c98c5f6e305 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Tue, 17 Dec 2024 01:19:51 +0100 Subject: [PATCH] USD: Add test for particle hair export/import Until the newer Hair Curves system can fully replace particle hair, add a small test to ensure this continues to work. Since the hair is exported as cubic bspline curves, we can also use this same file to test bspline import now too. Pull Request: https://projects.blender.org/blender/blender/pulls/131997 --- tests/python/bl_usd_export_test.py | 30 ++++++++++++++++++++ tests/python/bl_usd_import_test.py | 44 +++++++++++++++++++++++++----- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 4994c2bc7a7..4c57eba2a3d 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -444,6 +444,36 @@ class USDExportTest(AbstractUSDTest): expected_prim_verts = {"Ball_A": 2232, "Ball_B": 2876, "Ball_C": 1152} self.assertDictEqual(actual_prim_verts, expected_prim_verts) + def test_particle_hair(self): + """Validate correct export of particle hair emitters.""" + + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_particle_hair.blend")) + + # Ensure the hair dynamics are baked for all relevant frames... + for frame in range(1, 11): + bpy.context.scene.frame_set(frame) + bpy.context.scene.frame_set(1) + + export_path = self.tempdir / "usd_particle_hair.usda" + self.export_and_validate( + filepath=str(export_path), export_hair=True, export_animation=True, evaluation_mode="RENDER") + + stage = Usd.Stage.Open(str(export_path)) + main_prim = stage.GetPrimAtPath("/root/Sphere") + hair_prim = stage.GetPrimAtPath("/root/Sphere/ParticleSystem") + self.assertTrue(main_prim.IsValid()) + self.assertTrue(hair_prim.IsValid()) + + # Ensure we have 5 frames of rotation data for the main Sphere and 10 frames for the hair data + rot_samples = UsdGeom.Xformable(main_prim).GetRotateXYZOp().GetTimeSamples() + self.assertEqual(len(rot_samples), 5) + + hair_curves = UsdGeom.BasisCurves(hair_prim) + hair_samples = hair_curves.GetPointsAttr().GetTimeSamples() + self.assertEqual(hair_curves.GetTypeAttr().Get(), "cubic") + self.assertEqual(hair_curves.GetBasisAttr().Get(), "bspline") + self.assertEqual(len(hair_samples), 10) + def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len): pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name) self.assertTrue(pv.HasValue()) diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 6591f55abc6..2c132ac27d5 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -734,7 +734,7 @@ class USDImportTest(AbstractUSDTest): self.assertAlmostEqual(f.evaluate(10), 0.0, 2, "Unexpected value for rotation quaternion Z curve at frame 10") def check_curve(self, blender_curve, usd_curve): - curve_type_map = {"linear": 1, "cubic": 2} + curve_type_map = {"linear": 1, "cubic-bezier": 2, "cubic-bspline": 3} cyclic_map = {"nonperiodic": False, "periodic": True} # Check correct spline count. @@ -744,8 +744,11 @@ class USDImportTest(AbstractUSDTest): # Check correct type of curve. All splines should have the same type and periodicity. usd_curve_type = usd_curve.GetTypeAttr().Get() + usd_curve_type_basis = usd_curve_type + if usd_curve_type != "linear": + usd_curve_type_basis = usd_curve_type + "-" + usd_curve.GetBasisAttr().Get() usd_cyclic = usd_curve.GetWrapAttr().Get() - expected_curve_type = curve_type_map[usd_curve_type] + expected_curve_type = curve_type_map[usd_curve_type_basis] expected_cyclic = cyclic_map[usd_cyclic] for i in range(0, blender_spline_count): @@ -762,10 +765,10 @@ class USDImportTest(AbstractUSDTest): blender_positions = blender_curve.attributes["position"].data point_count = 0 - if usd_curve_type == "linear": + if usd_curve_type_basis == "linear": point_count = len(usd_positions) self.assertEqual(len(blender_positions), point_count) - elif usd_curve_type == "cubic": + elif usd_curve_type_basis == "cubic-bezier": control_point_count = 0 usd_vert_counts = usd_curve.GetCurveVertexCountsAttr().Get() for i in range(0, usd_spline_count): @@ -776,19 +779,25 @@ class USDImportTest(AbstractUSDTest): point_count = control_point_count self.assertEqual(len(blender_positions), point_count) + elif usd_curve_type_basis == "cubic-bspline": + point_count = len(usd_positions) + self.assertEqual(len(blender_positions), point_count) + + # Check radius data. (note: the currently available bsplines have no radii) + if usd_curve_type_basis == "cubic-bspline": + return - # Check radius data. usd_width_interpolation = usd_curve.GetWidthsInterpolation() usd_radius = [w / 2 for w in usd_curve.GetWidthsAttr().Get()] blender_radius = [r.value for r in blender_curve.attributes["radius"].data] - if usd_curve_type == "linear": + if usd_curve_type_basis == "linear": if usd_width_interpolation == "constant": usd_radius = usd_radius * point_count for i in range(0, len(blender_radius)): self.assertAlmostEqual(blender_radius[i], usd_radius[i], 2) - elif usd_curve_type == "cubic": + elif usd_curve_type_basis == "cubic-bezier": if usd_width_interpolation == "constant": usd_radius = usd_radius * point_count @@ -911,6 +920,27 @@ class USDImportTest(AbstractUSDTest): usd_prim = stage.GetPrimAtPath("/root/bezier_periodic/multiple/bezier_periodic_multiple_vertex") self.check_curve(blender_curve, UsdGeom.BasisCurves(usd_prim)) + def test_import_curves_bspline(self): + """Test importing bspline curve variations.""" + + # Use the existing hair test file to create the USD file + # for import. It is validated as part of the bl_usd_export test. + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_particle_hair.blend")) + testfile = str(self.tempdir / "usd_particle_hair.usda") + res = bpy.ops.wm.usd_export(filepath=testfile, export_hair=True, evaluation_mode="RENDER") + self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}") + + # Reload the empty file and import back in + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend")) + res = bpy.ops.wm.usd_import(filepath=testfile) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}") + + stage = Usd.Stage.Open(testfile) + + blender_curve = bpy.data.objects["ParticleSystem"].data + usd_prim = stage.GetPrimAtPath("/root/Sphere/ParticleSystem") + self.check_curve(blender_curve, UsdGeom.BasisCurves(usd_prim)) + def test_import_point_instancer(self): """Test importing a typical point instancer setup."""