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
This commit is contained in:
Jesse Yurkovich
2024-12-17 01:19:51 +01:00
committed by Jesse Yurkovich
parent c42894a695
commit e19f9b32a8
2 changed files with 67 additions and 7 deletions

View File

@@ -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())

View File

@@ -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."""