USD: Add test coverage for parent-child transforms during animation

Makes use of recently added test data to ensure proper handling of child
objects, with animated transform constraints, parented to other objects
who also have animated transform constraints.

Also uses the `colored_print` module to better segment the test output.

Pull Request: https://projects.blender.org/blender/blender/pulls/133600
This commit is contained in:
Jesse Yurkovich
2025-01-26 04:43:16 +01:00
committed by Jesse Yurkovich
parent 3e11cfb1d0
commit d4c2d73864
4 changed files with 53 additions and 10 deletions

View File

@@ -3,6 +3,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import math
import os
import pathlib
import pprint
import sys
@@ -12,6 +13,10 @@ from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdSkel, UsdUtils, UsdVol
import bpy
sys.path.append(str(pathlib.Path(__file__).parent.absolute()))
from modules.colored_print import (print_message, use_message_colors)
args = None
@@ -21,17 +26,23 @@ class AbstractUSDTest(unittest.TestCase):
cls._tempdir = tempfile.TemporaryDirectory()
cls.testdir = args.testdir
cls.tempdir = pathlib.Path(cls._tempdir.name)
return cls
if os.environ.get("BLENDER_TEST_COLOR") is not None:
use_message_colors()
def setUp(self):
self.assertTrue(
self.testdir.exists(), "Test dir {0} should exist".format(self.testdir)
)
self.assertTrue(self.testdir.exists(), "Test dir {0} should exist".format(self.testdir))
print_message(self._testMethodName, 'SUCCESS', 'RUN')
def tearDown(self):
self._tempdir.cleanup()
result = self._outcome.result
ok = all(test != self for test, _ in result.errors + result.failures)
if not ok:
print_message(self._testMethodName, 'FAILURE', 'FAILED')
else:
print_message(self._testMethodName, 'SUCCESS', 'PASSED')
def export_and_validate(self, **kwargs):
"""Export and validate the resulting USD file."""
@@ -926,6 +937,15 @@ class USDExportTest(AbstractUSDTest):
self.assertEqual(rot_samples, [1.0])
self.assertEqual(scale_samples, [1.0])
prim = stage.GetPrimAtPath("/root/cube_anim_xform/cube_anim_child")
self.assertEqual(prim.GetTypeName(), "Xform")
loc_samples = UsdGeom.Xformable(prim).GetTranslateOp().GetTimeSamples()
rot_samples = UsdGeom.Xformable(prim).GetRotateXYZOp().GetTimeSamples()
scale_samples = UsdGeom.Xformable(prim).GetScaleOp().GetTimeSamples()
self.assertEqual(loc_samples, [1.0])
self.assertEqual(rot_samples, [1.0, 2.0, 3.0, 4.0])
self.assertEqual(scale_samples, [1.0])
# Validate the armature animation
prim = stage.GetPrimAtPath("/root/Armature/Armature")
self.assertEqual(prim.GetTypeName(), "Skeleton")
@@ -1548,7 +1568,7 @@ def main():
parser.add_argument("--testdir", required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
unittest.main(argv=remaining, verbosity=0)
if __name__ == "__main__":

View File

@@ -3,6 +3,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import math
import os
import pathlib
import sys
import tempfile
@@ -11,6 +12,10 @@ from pxr import Ar, Gf, Sdf, Usd, UsdGeom, UsdShade
import bpy
sys.path.append(str(pathlib.Path(__file__).parent.absolute()))
from modules.colored_print import (print_message, use_message_colors)
args = None
@@ -18,6 +23,8 @@ class AbstractUSDTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.testdir = args.testdir
if os.environ.get("BLENDER_TEST_COLOR") is not None:
use_message_colors()
def setUp(self):
self._tempdir = tempfile.TemporaryDirectory()
@@ -28,12 +35,21 @@ class AbstractUSDTest(unittest.TestCase):
self.assertTrue(self.tempdir.exists(),
'Temp dir {0} should exist'.format(self.tempdir))
print_message(self._testMethodName, 'SUCCESS', 'RUN')
# Make sure we always start with a known-empty file.
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
def tearDown(self):
self._tempdir.cleanup()
result = self._outcome.result
ok = all(test != self for test, _ in result.errors + result.failures)
if not ok:
print_message(self._testMethodName, 'FAILURE', 'FAILED')
else:
print_message(self._testMethodName, 'SUCCESS', 'PASSED')
class USDImportTest(AbstractUSDTest):
# Utility function to round each component of a vector to a few digits. The "+ 0" is to
@@ -577,6 +593,7 @@ class USDImportTest(AbstractUSDTest):
# Validate some simple aspects of the animated objects which prove that they're animating.
ob_xform = bpy.data.objects["cube_anim_xform"]
ob_xform_child = bpy.data.objects["cube_anim_child_mesh"]
ob_shapekeys = bpy.data.objects["cube_anim_keys"]
ob_arm = bpy.data.objects["column_anim_armature"]
ob_arm2_side_a = bpy.data.objects["side_a"]
@@ -585,7 +602,10 @@ class USDImportTest(AbstractUSDTest):
bpy.context.scene.frame_set(1)
self.assertEqual(len(ob_xform.constraints), 1)
self.assertEqual(len(ob_xform_child.constraints), 1)
self.assertEqual(self.round_vector(ob_xform.matrix_world.translation), [0.0, -2.0, 0.0])
self.assertEqual(self.round_vector(ob_xform_child.matrix_world.translation), [0.0, -2.0, 1.0])
self.assertEqual(self.round_vector(ob_xform_child.matrix_world.to_euler('XYZ')), [0.0, 0.0, 0.0])
self.assertEqual(self.round_vector(ob_shapekeys.dimensions), [1.0, 1.0, 1.0])
self.assertEqual(self.round_vector(ob_arm.dimensions), [0.4, 0.4, 3.0])
self.assertEqual(self.round_vector(ob_arm2_side_a.dimensions), [0.5, 0.0, 0.5])
@@ -595,7 +615,10 @@ class USDImportTest(AbstractUSDTest):
bpy.context.scene.frame_set(5)
self.assertEqual(len(ob_xform.constraints), 1)
self.assertEqual(len(ob_xform_child.constraints), 1)
self.assertEqual(self.round_vector(ob_xform.matrix_world.translation), [3.0, -2.0, 0.0])
self.assertEqual(self.round_vector(ob_xform_child.matrix_world.translation), [3.0, -2.0, 1.0])
self.assertEqual(self.round_vector(ob_xform_child.matrix_world.to_euler('XYZ')), [0.0, 1.5708, 0.0])
self.assertEqual(self.round_vector(ob_shapekeys.dimensions), [0.1, 0.1, 0.1])
self.assertEqual(self.round_vector(ob_arm.dimensions), [1.65545, 0.4, 2.38953])
self.assertEqual(self.round_vector(ob_arm2_side_a.dimensions), [0.25, 0.0, 0.25])
@@ -1911,7 +1934,7 @@ def main():
parser.add_argument('--testdir', required=True, type=pathlib.Path)
args, remaining = parser.parse_known_args(argv)
unittest.main(argv=remaining)
unittest.main(argv=remaining, verbosity=0)
if __name__ == "__main__":

View File

@@ -36,9 +36,9 @@ def print_message(message, type=None, status=''):
elif status == 'OK':
status_text = " OK "
elif status == 'PASSED':
status_text = " PASSED "
status_text = " PASSED "
elif status == 'FAILED':
status_text = " FAILED "
status_text = " FAILED "
else:
status_text = status
if status_text: