USD: Test coverage for custom properties, xform ops, and orientation

Additional coverage for the following scenarios:
- Ensures custom properties are exported and imported correctly
- Ensures that xform op modes and scene orientation options are properly
  respected during export

Pull Request: https://projects.blender.org/blender/blender/pulls/126723
This commit is contained in:
Jesse Yurkovich
2024-08-24 06:58:27 +02:00
committed by Jesse Yurkovich
parent 3a81bde896
commit 409abccadd
2 changed files with 206 additions and 12 deletions

View File

@@ -2,17 +2,13 @@
#
# SPDX-License-Identifier: GPL-2.0-or-later
import math
import pathlib
import pprint
import sys
import tempfile
import unittest
from pxr import Usd
from pxr import UsdUtils
from pxr import UsdGeom
from pxr import UsdShade
from pxr import UsdSkel
from pxr import Gf
from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdSkel, UsdUtils
import bpy
@@ -79,6 +75,13 @@ class USDExportTest(AbstractUSDTest):
self.assertFalse(failed_checks, pprint.pformat(failed_checks))
# Utility function to round each component of a vector to a few digits. The "+ 0" is to
# ensure that any negative zeros (-0.0) are converted to positive zeros (0.0).
@staticmethod
def round_vector(vector):
return [round(c, 4) + 0 for c in vector]
# Utility function to compare two Gf.Vec3d's
def compareVec3d(self, first, second):
places = 5
self.assertAlmostEqual(first[0], second[0], places)
@@ -444,7 +447,7 @@ class USDExportTest(AbstractUSDTest):
self.assertEqual(len(indices2), 15)
self.assertNotEqual(indices1, indices2)
def test_animation(self):
def test_export_animation(self):
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend"))
export_path = self.tempdir / "usd_anim_test.usda"
res = bpy.ops.wm.usd_export(
@@ -494,6 +497,93 @@ class USDExportTest(AbstractUSDTest):
weight_samples = anim.GetBlendShapeWeightsAttr().GetTimeSamples()
self.assertEqual(weight_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
def test_export_xform_ops(self):
"""Test exporting different xform operation modes."""
# Create a simple scene and export using each of our xform op modes
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
loc = [1, 2, 3]
rot = [math.pi / 4, 0, math.pi / 8]
scale = [1, 2, 3]
bpy.ops.mesh.primitive_plane_add(location=loc, rotation=rot)
bpy.data.objects[0].scale = scale
test_path1 = self.tempdir / "temp_xform_trs_test.usda"
res = bpy.ops.wm.usd_export(filepath=str(test_path1), xform_op_mode='TRS')
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path1}")
test_path2 = self.tempdir / "temp_xform_tos_test.usda"
res = bpy.ops.wm.usd_export(filepath=str(test_path2), xform_op_mode='TOS')
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path2}")
test_path3 = self.tempdir / "temp_xform_mat_test.usda"
res = bpy.ops.wm.usd_export(filepath=str(test_path3), xform_op_mode='MAT')
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path3}")
# Validate relevant details for each case
stage = Usd.Stage.Open(str(test_path1))
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
rot_degs = [math.degrees(rot[0]), math.degrees(rot[1]), math.degrees(rot[2])]
self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:translate', 'xformOp:rotateXYZ', 'xformOp:scale'])
self.assertEqual(self.round_vector(xf.GetTranslateOp().Get()), loc)
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), rot_degs)
self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
stage = Usd.Stage.Open(str(test_path2))
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
orient_quat = xf.GetOrientOp().Get()
self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:translate', 'xformOp:orient', 'xformOp:scale'])
self.assertEqual(self.round_vector(xf.GetTranslateOp().Get()), loc)
self.assertEqual(round(orient_quat.GetReal(), 4), 0.9061)
self.assertEqual(self.round_vector(orient_quat.GetImaginary()), [0.3753, 0.0747, 0.1802])
self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
stage = Usd.Stage.Open(str(test_path3))
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
mat = xf.GetTransformOp().Get()
mat = [
self.round_vector(mat[0]), self.round_vector(mat[1]), self.round_vector(mat[2]), self.round_vector(mat[3])
]
expected = [
[0.9239, 0.3827, 0.0, 0.0],
[-0.5412, 1.3066, 1.4142, 0.0],
[0.8118, -1.9598, 2.1213, 0.0],
[1.0, 2.0, 3.0, 1.0]
]
self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:transform'])
self.assertEqual(mat, expected)
def test_export_orientation(self):
"""Test exporting different orientation configurations."""
# Using the empty scene is fine for this
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
test_path1 = self.tempdir / "temp_orientation_yup.usda"
res = bpy.ops.wm.usd_export(
filepath=str(test_path1),
convert_orientation=True,
export_global_forward_selection='NEGATIVE_Z',
export_global_up_selection='Y')
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path1}")
test_path2 = self.tempdir / "temp_orientation_zup_rev.usda"
res = bpy.ops.wm.usd_export(
filepath=str(test_path2),
convert_orientation=True,
export_global_forward_selection='NEGATIVE_Y',
export_global_up_selection='Z')
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path2}")
stage = Usd.Stage.Open(str(test_path1))
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [-90, 0, 0])
stage = Usd.Stage.Open(str(test_path2))
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [0, 0, 180])
def test_materialx_network(self):
"""Test exporting that a MaterialX export makes it out alright"""
bpy.ops.wm.open_mainfile(

View File

@@ -5,12 +5,9 @@
import math
import pathlib
import sys
import unittest
import tempfile
from pxr import Usd
from pxr import UsdShade
from pxr import UsdGeom
from pxr import Sdf
import unittest
from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade
import bpy
@@ -920,6 +917,113 @@ class USDImportTest(AbstractUSDTest):
self.assertTrue(len(ob.modifiers) == 1 and ob.modifiers[0].type ==
'MESH_SEQUENCE_CACHE', f"{ob.name} has incorrect modifiers")
def test_import_id_props(self):
"""Test importing object and data IDProperties."""
# Create our set of ID's with all relevant IDProperty types/values that we support
bpy.ops.object.empty_add()
bpy.ops.object.light_add()
bpy.ops.object.camera_add()
bpy.ops.mesh.primitive_plane_add()
ids = [ob if ob.type == 'EMPTY' else ob.data for ob in bpy.data.objects]
properties = [
True, "string", 1, 2.0, [1, 2], [1, 2, 3], [1, 2, 3, 4], [1.0, 2.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0, 4.0]
]
for id in ids:
for i, p in enumerate(properties):
prop_name = "prop" + str(i)
id[prop_name] = p
# Export out this scene twice so we can test both the default "userProperties" namespace as
# well as a custom namespace
test_path1 = self.tempdir / "temp_idprops_userProperties_test.usda"
res = bpy.ops.wm.usd_export(filepath=str(test_path1), evaluation_mode="RENDER")
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path1}")
custom_namespace = "customns"
test_path2 = self.tempdir / "temp_idprops_customns_test.usda"
res = bpy.ops.wm.usd_export(
filepath=str(test_path2),
custom_properties_namespace=custom_namespace,
evaluation_mode="RENDER")
self.assertEqual({'FINISHED'}, res, f"Unable to export to {test_path2}")
# Also write out another file using attribute types not natively writable by Blender
test_path3 = self.tempdir / "temp_idprops_extended_test.usda"
stage = Usd.Stage.CreateNew(str(test_path3))
xform = UsdGeom.Xform.Define(stage, '/empty')
xform.GetPrim().CreateAttribute("prop0", Sdf.ValueTypeNames.Half).Set(0.5)
xform.GetPrim().CreateAttribute("prop1", Sdf.ValueTypeNames.Float).Set(1.5)
xform.GetPrim().CreateAttribute("prop2", Sdf.ValueTypeNames.Token).Set("tokenstring")
xform.GetPrim().CreateAttribute("prop3", Sdf.ValueTypeNames.Asset).Set("assetstring")
xform.GetPrim().CreateAttribute("prop4", Sdf.ValueTypeNames.Half2).Set(Gf.Vec2h(0, 1))
xform.GetPrim().CreateAttribute("prop5", Sdf.ValueTypeNames.Half3).Set(Gf.Vec3h(0, 1, 2))
xform.GetPrim().CreateAttribute("prop6", Sdf.ValueTypeNames.Half4).Set(Gf.Vec4h(0, 1, 2, 3))
xform.GetPrim().CreateAttribute("prop7", Sdf.ValueTypeNames.Float2).Set(Gf.Vec2f(0, 1))
xform.GetPrim().CreateAttribute("prop8", Sdf.ValueTypeNames.Float3).Set(Gf.Vec3f(0, 1, 2))
xform.GetPrim().CreateAttribute("prop9", Sdf.ValueTypeNames.Float4).Set(Gf.Vec4f(0, 1, 2, 3))
stage.GetRootLayer().Save()
# Helper functions to check IDProperty validity
import idprop
def assert_all_props_present(properties, ns):
ids = [ob if ob.type == 'EMPTY' else ob.data for ob in bpy.data.objects]
for id in ids:
for i, p in enumerate(properties):
prop_name = (ns + ":" if ns != "" else "") + "prop" + str(i)
prop = id[prop_name]
value = prop.to_list() if type(prop) is idprop.types.IDPropertyArray else prop
self.assertEqual(p, value, f"Property {prop_name} is incorrect")
def assert_no_props_present(properties, ns):
ids = [ob if ob.type == 'EMPTY' else ob.data for ob in bpy.data.objects]
for id in ids:
for i, p in enumerate(properties):
prop_name = (ns + ":" if ns != "" else "") + "prop" + str(i)
self.assertTrue(id.get(prop_name) is None, f"Property {prop_name} should not be present")
# Reload the empty file and test the relevant combinations of namespaces and import modes
infile = str(test_path1)
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=infile, attr_import_mode='USER')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
self.assertEqual(len(bpy.data.objects), 4)
assert_all_props_present(properties, "")
infile = str(test_path1)
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=infile, attr_import_mode='NONE')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
self.assertEqual(len(bpy.data.objects), 4)
assert_no_props_present(properties, "")
infile = str(test_path2)
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=infile, attr_import_mode='ALL')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
self.assertEqual(len(bpy.data.objects), 4)
assert_all_props_present(properties, custom_namespace)
infile = str(test_path2)
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=infile, attr_import_mode='USER')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
self.assertEqual(len(bpy.data.objects), 4)
assert_no_props_present(properties, custom_namespace)
infile = str(test_path3)
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
res = bpy.ops.wm.usd_import(filepath=infile, attr_import_mode='ALL')
self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {infile}")
self.assertEqual(len(bpy.data.objects), 1)
properties = [
0.5, 1.5, "tokenstring", "assetstring", [0, 1], [0, 1, 2], [0, 1, 2, 3], [0, 1], [0, 1, 2], [0, 1, 2, 3]
]
assert_all_props_present(properties, "")
def main():
global args