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:
committed by
Jesse Yurkovich
parent
3a81bde896
commit
409abccadd
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user