Cleanup: Remove unused include
[blender.git] / tests / python / bl_usd_export_test.py
blobc34145648bd5ba7369b862826bd135c1d76ea8e3
1 # SPDX-FileCopyrightText: 2023 Blender Authors
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import math
6 import os
7 import pathlib
8 import pprint
9 import sys
10 import tempfile
11 import unittest
12 from pxr import Gf, Sdf, Usd, UsdGeom, UsdShade, UsdSkel, UsdUtils, UsdVol
14 import bpy
16 sys.path.append(str(pathlib.Path(__file__).parent.absolute()))
17 from modules.colored_print import (print_message, use_message_colors)
20 args = None
23 class AbstractUSDTest(unittest.TestCase):
24 @classmethod
25 def setUpClass(cls):
26 cls._tempdir = tempfile.TemporaryDirectory()
27 cls.testdir = args.testdir
28 cls.tempdir = pathlib.Path(cls._tempdir.name)
29 if os.environ.get("BLENDER_TEST_COLOR") is not None:
30 use_message_colors()
32 def setUp(self):
33 self.assertTrue(self.testdir.exists(), "Test dir {0} should exist".format(self.testdir))
34 print_message(self._testMethodName, 'SUCCESS', 'RUN')
36 def tearDown(self):
37 self._tempdir.cleanup()
39 result = self._outcome.result
40 ok = all(test != self for test, _ in result.errors + result.failures)
41 if not ok:
42 print_message(self._testMethodName, 'FAILURE', 'FAILED')
43 else:
44 print_message(self._testMethodName, 'SUCCESS', 'PASSED')
46 def export_and_validate(self, **kwargs):
47 """Export and validate the resulting USD file."""
49 export_path = kwargs["filepath"]
51 # Do the actual export
52 res = bpy.ops.wm.usd_export(**kwargs)
53 self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
55 # Validate resulting file
56 checker = UsdUtils.ComplianceChecker(
57 arkit=False,
58 skipARKitRootLayerCheck=False,
59 rootPackageOnly=False,
60 skipVariants=False,
61 verbose=False,
63 checker.CheckCompliance(export_path)
65 failed_checks = {}
67 # The ComplianceChecker does not know how to resolve <UDIM> tags, so
68 # it will flag "textures/test_grid_<UDIM>.png" as a missing reference.
69 # That reference is in fact OK, so we skip the rule for this test.
70 to_skip = ("MissingReferenceChecker",)
71 for rule in checker._rules:
72 name = rule.__class__.__name__
73 if name in to_skip:
74 continue
76 issues = rule.GetFailedChecks() + rule.GetWarnings() + rule.GetErrors()
77 if not issues:
78 continue
80 failed_checks[name] = issues
82 self.assertFalse(failed_checks, pprint.pformat(failed_checks))
85 class USDExportTest(AbstractUSDTest):
86 # Utility function to round each component of a vector to a few digits. The "+ 0" is to
87 # ensure that any negative zeros (-0.0) are converted to positive zeros (0.0).
88 @staticmethod
89 def round_vector(vector):
90 return [round(c, 4) + 0 for c in vector]
92 # Utility function to compare two Gf.Vec3d's
93 def compareVec3d(self, first, second):
94 places = 5
95 self.assertAlmostEqual(first[0], second[0], places)
96 self.assertAlmostEqual(first[1], second[1], places)
97 self.assertAlmostEqual(first[2], second[2], places)
99 def test_export_extents(self):
100 """Test that exported scenes contain have a properly authored extent attribute on each boundable prim"""
101 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_extent_test.blend"))
102 export_path = self.tempdir / "usd_extent_test.usda"
104 self.export_and_validate(
105 filepath=str(export_path),
106 export_materials=True,
107 evaluation_mode="RENDER",
108 convert_world_material=False,
111 # if prims are missing, the exporter must have skipped some objects
112 stats = UsdUtils.ComputeUsdStageStats(str(export_path))
113 self.assertEqual(stats["totalPrimCount"], 16, "Unexpected number of prims")
115 # validate the overall world bounds of the scene
116 stage = Usd.Stage.Open(str(export_path))
117 scenePrim = stage.GetPrimAtPath("/root/scene")
118 bboxcache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_])
119 bounds = bboxcache.ComputeWorldBound(scenePrim)
120 bound_min = bounds.GetRange().GetMin()
121 bound_max = bounds.GetRange().GetMax()
122 self.compareVec3d(bound_min, Gf.Vec3d(-5.752975881, -1, -2.798513651))
123 self.compareVec3d(bound_max, Gf.Vec3d(1, 2.9515805244, 2.7985136508))
125 # validate the locally authored extents
126 prim = stage.GetPrimAtPath("/root/scene/BigCube/BigCubeMesh")
127 extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
128 self.compareVec3d(Gf.Vec3d(extent[0]), Gf.Vec3d(-1, -1, -2.7985137))
129 self.compareVec3d(Gf.Vec3d(extent[1]), Gf.Vec3d(1, 1, 2.7985137))
130 prim = stage.GetPrimAtPath("/root/scene/LittleCube/LittleCubeMesh")
131 extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
132 self.compareVec3d(Gf.Vec3d(extent[0]), Gf.Vec3d(-1, -1, -1))
133 self.compareVec3d(Gf.Vec3d(extent[1]), Gf.Vec3d(1, 1, 1))
134 prim = stage.GetPrimAtPath("/root/scene/Volume/Volume")
135 extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
136 self.compareVec3d(
137 Gf.Vec3d(extent[0]), Gf.Vec3d(-0.7313742, -0.68043584, -0.5801515)
139 self.compareVec3d(
140 Gf.Vec3d(extent[1]), Gf.Vec3d(0.7515701, 0.5500924, 0.9027928)
143 def test_material_transforms(self):
144 """Validate correct export of image mapping parameters to the UsdTransform2d shader def"""
146 # Use the common materials .blend file
147 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
148 export_path = self.tempdir / "material_transforms.usda"
149 self.export_and_validate(filepath=str(export_path), export_materials=True)
151 # Inspect the UsdTransform2d prim on the "Transforms" material
152 stage = Usd.Stage.Open(str(export_path))
153 shader_prim = stage.GetPrimAtPath("/root/_materials/Transforms/Mapping")
154 shader = UsdShade.Shader(shader_prim)
155 self.assertEqual(shader.GetIdAttr().Get(), "UsdTransform2d")
156 input_trans = shader.GetInput('translation')
157 input_rot = shader.GetInput('rotation')
158 input_scale = shader.GetInput('scale')
159 self.assertEqual(input_trans.Get(), [0.75, 0.75])
160 self.assertEqual(input_rot.Get(), 180)
161 self.assertEqual(input_scale.Get(), [0.5, 0.5])
163 def test_material_normal_maps(self):
164 """Validate correct export of typical normal map setups to the UsdUVTexture shader def.
165 Namely validate that scale, bias, and ColorSpace settings are correct"""
167 # Use the common materials .blend file
168 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
169 export_path = self.tempdir / "material_normalmaps.usda"
170 self.export_and_validate(filepath=str(export_path), export_materials=True)
172 # Inspect the UsdUVTexture prim on the "typical" "NormalMap" material
173 stage = Usd.Stage.Open(str(export_path))
174 shader_prim = stage.GetPrimAtPath("/root/_materials/NormalMap/Image_Texture")
175 shader = UsdShade.Shader(shader_prim)
176 self.assertEqual(shader.GetIdAttr().Get(), "UsdUVTexture")
177 input_scale = shader.GetInput('scale')
178 input_bias = shader.GetInput('bias')
179 input_colorspace = shader.GetInput('sourceColorSpace')
180 self.assertEqual(input_scale.Get(), [2, 2, 2, 2])
181 self.assertEqual(input_bias.Get(), [-1, -1, -1, -1])
182 self.assertEqual(input_colorspace.Get(), 'raw')
184 # Inspect the UsdUVTexture prim on the "inverted" "NormalMap_Scale_Bias" material
185 stage = Usd.Stage.Open(str(export_path))
186 shader_prim = stage.GetPrimAtPath("/root/_materials/NormalMap_Scale_Bias/Image_Texture")
187 shader = UsdShade.Shader(shader_prim)
188 self.assertEqual(shader.GetIdAttr().Get(), "UsdUVTexture")
189 input_scale = shader.GetInput('scale')
190 input_bias = shader.GetInput('bias')
191 input_colorspace = shader.GetInput('sourceColorSpace')
192 self.assertEqual(input_scale.Get(), [2, -2, 2, 1])
193 self.assertEqual(input_bias.Get(), [-1, 1, -1, 0])
194 self.assertEqual(input_colorspace.Get(), 'raw')
196 def test_material_opacity_threshold(self):
197 """Validate correct export of opacity and opacity_threshold parameters to the UsdPreviewSurface shader def"""
199 # Use the common materials .blend file
200 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_channels.blend"))
201 export_path = self.tempdir / "usd_materials_channels.usda"
202 self.export_and_validate(filepath=str(export_path), export_materials=True)
204 # Opaque no-Alpha
205 stage = Usd.Stage.Open(str(export_path))
206 shader_prim = stage.GetPrimAtPath("/root/_materials/Opaque/Principled_BSDF")
207 shader = UsdShade.Shader(shader_prim)
208 opacity_input = shader.GetInput('opacity')
209 self.assertEqual(opacity_input.HasConnectedSource(), False,
210 "Opacity input should not be connected for opaque material")
211 self.assertAlmostEqual(opacity_input.Get(), 1.0, 2, "Opacity input should be set to 1")
213 # Validate Image Alpha to BSDF Alpha
214 shader_prim = stage.GetPrimAtPath("/root/_materials/Alpha/Principled_BSDF")
215 shader = UsdShade.Shader(shader_prim)
216 opacity_input = shader.GetInput('opacity')
217 opacity_thresh_input = shader.GetInput('opacityThreshold')
218 self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
219 self.assertEqual(opacity_thresh_input.Get(), None, "Opacity threshold input should be empty")
221 # Validate Image Alpha to BSDF Alpha w/Round
222 shader_prim = stage.GetPrimAtPath("/root/_materials/AlphaClip_Round/Principled_BSDF")
223 shader = UsdShade.Shader(shader_prim)
224 opacity_input = shader.GetInput('opacity')
225 opacity_thresh_input = shader.GetInput('opacityThreshold')
226 self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
227 self.assertAlmostEqual(opacity_thresh_input.Get(), 0.5, 2, "Opacity threshold input should be 0.5")
229 # Validate Image Alpha to BSDF Alpha w/LessThan+Invert
230 shader_prim = stage.GetPrimAtPath("/root/_materials/AlphaClip_LessThan/Principled_BSDF")
231 shader = UsdShade.Shader(shader_prim)
232 opacity_input = shader.GetInput('opacity')
233 opacity_thresh_input = shader.GetInput('opacityThreshold')
234 self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
235 self.assertAlmostEqual(opacity_thresh_input.Get(), 0.8, 2, "Opacity threshold input should be 0.8")
237 # Validate Image RGB to BSDF Metallic, Roughness, Alpha
238 shader_prim = stage.GetPrimAtPath("/root/_materials/Channel/Principled_BSDF")
239 shader = UsdShade.Shader(shader_prim)
240 metallic_input = shader.GetInput("metallic")
241 roughness_input = shader.GetInput("roughness")
242 opacity_input = shader.GetInput('opacity')
243 opacity_thresh_input = shader.GetInput('opacityThreshold')
244 self.assertEqual(metallic_input.HasConnectedSource(), True, "Metallic input should be connected")
245 self.assertEqual(roughness_input.HasConnectedSource(), True, "Roughness input should be connected")
246 self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
247 self.assertEqual(opacity_thresh_input.Get(), None, "Opacity threshold input should be empty")
249 # Validate Image RGB to BSDF Metallic, Roughness, Alpha w/Round
250 shader_prim = stage.GetPrimAtPath("/root/_materials/ChannelClip_Round/Principled_BSDF")
251 shader = UsdShade.Shader(shader_prim)
252 metallic_input = shader.GetInput("metallic")
253 roughness_input = shader.GetInput("roughness")
254 opacity_input = shader.GetInput('opacity')
255 opacity_thresh_input = shader.GetInput('opacityThreshold')
256 self.assertEqual(metallic_input.HasConnectedSource(), True, "Metallic input should be connected")
257 self.assertEqual(roughness_input.HasConnectedSource(), True, "Roughness input should be connected")
258 self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
259 self.assertAlmostEqual(opacity_thresh_input.Get(), 0.5, 2, "Opacity threshold input should be 0.5")
261 # Validate Image RGB to BSDF Metallic, Roughness, Alpha w/LessThan+Invert
262 shader_prim = stage.GetPrimAtPath("/root/_materials/ChannelClip_LessThan/Principled_BSDF")
263 shader = UsdShade.Shader(shader_prim)
264 metallic_input = shader.GetInput("metallic")
265 roughness_input = shader.GetInput("roughness")
266 opacity_input = shader.GetInput('opacity')
267 opacity_thresh_input = shader.GetInput('opacityThreshold')
268 self.assertEqual(metallic_input.HasConnectedSource(), True, "Metallic input should be connected")
269 self.assertEqual(roughness_input.HasConnectedSource(), True, "Roughness input should be connected")
270 self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
271 self.assertAlmostEqual(opacity_thresh_input.Get(), 0.2, 2, "Opacity threshold input should be 0.2")
273 def test_export_material_subsets(self):
274 """Validate multiple materials assigned to the same mesh work correctly."""
276 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_multi.blend"))
278 # Ensure the simulation zone data is baked for all relevant frames...
279 for frame in range(1, 5):
280 bpy.context.scene.frame_set(frame)
281 bpy.context.scene.frame_set(1)
283 export_path = self.tempdir / "usd_materials_multi.usda"
284 self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
286 stage = Usd.Stage.Open(str(export_path))
288 # The static mesh should have 4 materials each assigned to 4 faces (16 faces total)
289 static_mesh_prim = UsdGeom.Mesh(stage.GetPrimAtPath("/root/static_mesh/static_mesh"))
290 geom_subsets = UsdGeom.Subset.GetGeomSubsets(static_mesh_prim)
291 self.assertEqual(len(geom_subsets), 4)
293 unique_face_indices = set()
294 for subset in geom_subsets:
295 face_indices = subset.GetIndicesAttr().Get()
296 self.assertEqual(len(face_indices), 4)
297 unique_face_indices.update(face_indices)
298 self.assertEqual(len(unique_face_indices), 16)
300 # The dynamic mesh varies over time (currently blocked, see #124554 and #118754)
301 # - Frame 1: 1 face and 1 material [mat2]
302 # - Frame 2: 2 faces and 2 materials [mat2, mat3]
303 # - Frame 3: 4 faces and 3 materials [mat2, mat3, mat2, mat1]
304 # - Frame 4: 4 faces and 2 materials [mat2, mat3, mat2, mat3]
305 dynamic_mesh_prim = UsdGeom.Mesh(stage.GetPrimAtPath("/root/dynamic_mesh/dynamic_mesh"))
306 geom_subsets = UsdGeom.Subset.GetGeomSubsets(dynamic_mesh_prim)
307 self.assertEqual(len(geom_subsets), 0)
309 def test_export_material_inmem(self):
310 """Validate correct export of in memory and packed images"""
312 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_inmem_pack.blend"))
313 export_path1 = self.tempdir / "usd_materials_inmem_pack_relative.usda"
314 self.export_and_validate(filepath=str(export_path1), export_textures_mode='NEW', relative_paths=True)
316 export_path2 = self.tempdir / "usd_materials_inmem_pack_absolute.usda"
317 self.export_and_validate(filepath=str(export_path2), export_textures_mode='NEW', relative_paths=False)
319 # Validate that we actually see the correct set of files being saved to the filesystem
321 # Relative path variations
322 stage = Usd.Stage.Open(str(export_path1))
323 stage_path = pathlib.Path(stage.GetRootLayer().realPath)
325 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_single/Image_Texture")
326 shader = UsdShade.Shader(shader_prim)
327 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
328 self.assertFalse(asset_path.is_absolute())
329 self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
331 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_udim/Image_Texture")
332 shader = UsdShade.Shader(shader_prim)
333 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
334 image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
335 image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
336 self.assertFalse(asset_path.is_absolute())
337 self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
338 self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
340 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_single/Image_Texture")
341 shader = UsdShade.Shader(shader_prim)
342 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
343 self.assertFalse(asset_path.is_absolute())
344 self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
346 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_udim/Image_Texture")
347 shader = UsdShade.Shader(shader_prim)
348 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
349 image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
350 image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
351 self.assertFalse(asset_path.is_absolute())
352 self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
353 self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
355 # Absolute path variations
356 stage = Usd.Stage.Open(str(export_path2))
357 stage_path = pathlib.Path(stage.GetRootLayer().realPath)
359 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_single/Image_Texture")
360 shader = UsdShade.Shader(shader_prim)
361 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
362 self.assertTrue(asset_path.is_absolute())
363 self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
365 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_udim/Image_Texture")
366 shader = UsdShade.Shader(shader_prim)
367 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
368 image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
369 image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
370 self.assertTrue(asset_path.is_absolute())
371 self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
372 self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
374 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_single/Image_Texture")
375 shader = UsdShade.Shader(shader_prim)
376 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
377 self.assertTrue(asset_path.is_absolute())
378 self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
380 shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_udim/Image_Texture")
381 shader = UsdShade.Shader(shader_prim)
382 asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
383 image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
384 image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
385 self.assertTrue(asset_path.is_absolute())
386 self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
387 self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
389 def test_export_material_textures_mode(self):
390 """Validate the non-default export textures mode options."""
392 # Use the common materials .blend file
393 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
395 # For this test, the "textures" directory should NOT exist and the image paths
396 # should all point to the original test location, not the temp output location.
397 def check_image_paths(stage):
398 orig_tex_path = (self.testdir / "textures")
399 temp_tex_path = (self.tempdir / "textures")
400 self.assertFalse(temp_tex_path.is_dir())
402 shader_prim = stage.GetPrimAtPath("/root/_materials/Material/Image_Texture")
403 shader = UsdShade.Shader(shader_prim)
404 filepath = pathlib.Path(shader.GetInput('file').Get().path)
405 self.assertEqual(orig_tex_path, filepath.parent)
407 export_file = str(self.tempdir / "usd_materials_texture_preserve.usda")
408 self.export_and_validate(
409 filepath=export_file, export_materials=True, convert_world_material=False, export_textures_mode='PRESERVE')
410 check_image_paths(Usd.Stage.Open(export_file))
412 export_file = str(self.tempdir / "usd_materials_texture_keep.usda")
413 self.export_and_validate(
414 filepath=export_file, export_materials=True, convert_world_material=False, export_textures_mode='KEEP')
415 check_image_paths(Usd.Stage.Open(export_file))
417 def test_export_material_displacement(self):
418 """Validate correct export of Displacement information for the UsdPreviewSurface"""
420 # Use the common materials .blend file
421 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_displace.blend"))
422 export_path = self.tempdir / "material_displace.usda"
423 self.export_and_validate(filepath=str(export_path), export_materials=True)
425 stage = Usd.Stage.Open(str(export_path))
427 # Verify "constant" displacement
428 shader_surface = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/constant/Principled_BSDF"))
429 self.assertEqual(shader_surface.GetIdAttr().Get(), "UsdPreviewSurface")
430 input_displacement = shader_surface.GetInput('displacement')
431 self.assertEqual(input_displacement.HasConnectedSource(), False, "Displacement input should not be connected")
432 self.assertAlmostEqual(input_displacement.Get(), 0.45, 5)
434 # Validate various Midlevel and Scale scenarios
435 def validate_displacement(mat_name, expected_scale, expected_bias):
436 shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/{mat_name}/Principled_BSDF"))
437 shader_image = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/{mat_name}/Image_Texture"))
438 self.assertEqual(shader_surface.GetIdAttr().Get(), "UsdPreviewSurface")
439 self.assertEqual(shader_image.GetIdAttr().Get(), "UsdUVTexture")
440 input_displacement = shader_surface.GetInput('displacement')
441 input_colorspace = shader_image.GetInput('sourceColorSpace')
442 input_scale = shader_image.GetInput('scale')
443 input_bias = shader_image.GetInput('bias')
444 self.assertEqual(input_displacement.HasConnectedSource(), True, "Displacement input should be connected")
445 self.assertEqual(input_colorspace.Get(), 'raw')
446 self.assertEqual(self.round_vector(input_scale.Get()), expected_scale)
447 self.assertEqual(self.round_vector(input_bias.Get()), expected_bias)
449 validate_displacement("mid_0_0", [1.0, 1.0, 1.0, 1.0], [0, 0, 0, 0])
450 validate_displacement("mid_0_5", [1.0, 1.0, 1.0, 1.0], [-0.5, -0.5, -0.5, 0])
451 validate_displacement("mid_1_0", [1.0, 1.0, 1.0, 1.0], [-1, -1, -1, 0])
452 validate_displacement("mid_0_0_scale_0_3", [0.3, 0.3, 0.3, 1.0], [0, 0, 0, 0])
453 validate_displacement("mid_0_5_scale_0_3", [0.3, 0.3, 0.3, 1.0], [-0.15, -0.15, -0.15, 0])
454 validate_displacement("mid_1_0_scale_0_3", [0.3, 0.3, 0.3, 1.0], [-0.3, -0.3, -0.3, 0])
456 # Validate that no displacement occurs for scenarios USD doesn't support
457 shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/bad_wrong_space/Principled_BSDF"))
458 input_displacement = shader_surface.GetInput('displacement')
459 self.assertTrue(input_displacement.Get() is None)
460 shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/bad_non_const/Principled_BSDF"))
461 input_displacement = shader_surface.GetInput('displacement')
462 self.assertTrue(input_displacement.Get() is None)
464 def test_export_metaballs(self):
465 """Validate correct export of Metaball objects. These are written out as Meshes."""
467 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_metaballs.blend"))
468 export_path = self.tempdir / "usd_metaballs.usda"
469 self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER")
471 stage = Usd.Stage.Open(str(export_path))
473 # There should be 3 Mesh prims and they should each correspond to the "basis"
474 # metaball (i.e. the ones without any numeric suffix)
475 mesh_prims = [prim for prim in stage.Traverse() if prim.IsA(UsdGeom.Mesh)]
476 prim_names = [prim.GetPath().pathString for prim in mesh_prims]
477 self.assertEqual(len(mesh_prims), 3)
478 self.assertListEqual(
479 sorted(prim_names), ["/root/Ball_A/Ball_A", "/root/Ball_B/Ball_B", "/root/Ball_C/Ball_C"])
481 # Make rough check of vertex counts to ensure geometry is present
482 actual_prim_verts = {prim.GetName(): len(UsdGeom.Mesh(prim).GetPointsAttr().Get()) for prim in mesh_prims}
483 expected_prim_verts = {"Ball_A": 2232, "Ball_B": 2876, "Ball_C": 1152}
484 self.assertDictEqual(actual_prim_verts, expected_prim_verts)
486 def test_particle_hair(self):
487 """Validate correct export of particle hair emitters."""
489 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_particle_hair.blend"))
491 # Ensure the hair dynamics are baked for all relevant frames...
492 for frame in range(1, 11):
493 bpy.context.scene.frame_set(frame)
494 bpy.context.scene.frame_set(1)
496 export_path = self.tempdir / "usd_particle_hair.usda"
497 self.export_and_validate(
498 filepath=str(export_path), export_hair=True, export_animation=True, evaluation_mode="RENDER")
500 stage = Usd.Stage.Open(str(export_path))
501 main_prim = stage.GetPrimAtPath("/root/Sphere")
502 hair_prim = stage.GetPrimAtPath("/root/Sphere/ParticleSystem")
503 self.assertTrue(main_prim.IsValid())
504 self.assertTrue(hair_prim.IsValid())
506 # Ensure we have 5 frames of rotation data for the main Sphere and 10 frames for the hair data
507 rot_samples = UsdGeom.Xformable(main_prim).GetRotateXYZOp().GetTimeSamples()
508 self.assertEqual(len(rot_samples), 5)
510 hair_curves = UsdGeom.BasisCurves(hair_prim)
511 hair_samples = hair_curves.GetPointsAttr().GetTimeSamples()
512 self.assertEqual(hair_curves.GetTypeAttr().Get(), "cubic")
513 self.assertEqual(hair_curves.GetBasisAttr().Get(), "bspline")
514 self.assertEqual(len(hair_samples), 10)
516 def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len):
517 pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)
518 self.assertTrue(pv.HasValue())
519 self.assertEqual(pv.GetTypeName().type.typeName, pv_typeName)
520 self.assertEqual(pv.GetInterpolation(), pv_interp)
521 self.assertEqual(len(pv.Get()), elements_len)
523 def check_primvar_missing(self, prim, pv_name):
524 pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)
525 self.assertFalse(pv.HasValue())
527 def test_export_attributes(self):
528 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_test.blend"))
529 export_path = self.tempdir / "usd_attribute_test.usda"
530 self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER")
532 stage = Usd.Stage.Open(str(export_path))
534 # Validate all expected Mesh attributes. Notice that nothing on
535 # the Edge domain is supported by USD.
536 prim = stage.GetPrimAtPath("/root/Mesh/Mesh")
538 self.check_primvar(prim, "p_bool", "VtArray<bool>", "vertex", 4)
539 self.check_primvar(prim, "p_int8", "VtArray<unsigned char>", "vertex", 4)
540 self.check_primvar(prim, "p_int32", "VtArray<int>", "vertex", 4)
541 self.check_primvar(prim, "p_float", "VtArray<float>", "vertex", 4)
542 self.check_primvar(prim, "p_color", "VtArray<GfVec4f>", "vertex", 4)
543 self.check_primvar(prim, "p_byte_color", "VtArray<GfVec4f>", "vertex", 4)
544 self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "vertex", 4)
545 self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "vertex", 4)
546 self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "vertex", 4)
547 self.check_primvar_missing(prim, "p_mat4x4")
549 self.check_primvar_missing(prim, "e_bool")
550 self.check_primvar_missing(prim, "e_int8")
551 self.check_primvar_missing(prim, "e_int32")
552 self.check_primvar_missing(prim, "e_float")
553 self.check_primvar_missing(prim, "e_color")
554 self.check_primvar_missing(prim, "e_byte_color")
555 self.check_primvar_missing(prim, "e_vec2")
556 self.check_primvar_missing(prim, "e_vec3")
557 self.check_primvar_missing(prim, "e_quat")
558 self.check_primvar_missing(prim, "e_mat4x4")
560 self.check_primvar(prim, "f_bool", "VtArray<bool>", "uniform", 1)
561 self.check_primvar(prim, "f_int8", "VtArray<unsigned char>", "uniform", 1)
562 self.check_primvar(prim, "f_int32", "VtArray<int>", "uniform", 1)
563 self.check_primvar(prim, "f_float", "VtArray<float>", "uniform", 1)
564 self.check_primvar(prim, "f_color", "VtArray<GfVec4f>", "uniform", 1)
565 self.check_primvar(prim, "f_byte_color", "VtArray<GfVec4f>", "uniform", 1)
566 self.check_primvar(prim, "displayColor", "VtArray<GfVec3f>", "uniform", 1)
567 self.check_primvar(prim, "f_vec2", "VtArray<GfVec2f>", "uniform", 1)
568 self.check_primvar(prim, "f_vec3", "VtArray<GfVec3f>", "uniform", 1)
569 self.check_primvar(prim, "f_quat", "VtArray<GfQuatf>", "uniform", 1)
570 self.check_primvar_missing(prim, "f_mat4x4")
572 self.check_primvar(prim, "fc_bool", "VtArray<bool>", "faceVarying", 4)
573 self.check_primvar(prim, "fc_int8", "VtArray<unsigned char>", "faceVarying", 4)
574 self.check_primvar(prim, "fc_int32", "VtArray<int>", "faceVarying", 4)
575 self.check_primvar(prim, "fc_float", "VtArray<float>", "faceVarying", 4)
576 self.check_primvar(prim, "fc_color", "VtArray<GfVec4f>", "faceVarying", 4)
577 self.check_primvar(prim, "fc_byte_color", "VtArray<GfVec4f>", "faceVarying", 4)
578 self.check_primvar(prim, "fc_vec2", "VtArray<GfVec2f>", "faceVarying", 4)
579 self.check_primvar(prim, "fc_vec3", "VtArray<GfVec3f>", "faceVarying", 4)
580 self.check_primvar(prim, "fc_quat", "VtArray<GfQuatf>", "faceVarying", 4)
581 self.check_primvar_missing(prim, "fc_mat4x4")
583 prim = stage.GetPrimAtPath("/root/Curve_base/Curves/Curves")
585 self.check_primvar(prim, "p_bool", "VtArray<bool>", "vertex", 24)
586 self.check_primvar(prim, "p_int8", "VtArray<unsigned char>", "vertex", 24)
587 self.check_primvar(prim, "p_int32", "VtArray<int>", "vertex", 24)
588 self.check_primvar(prim, "p_float", "VtArray<float>", "vertex", 24)
589 self.check_primvar(prim, "p_color", "VtArray<GfVec4f>", "vertex", 24)
590 self.check_primvar(prim, "p_byte_color", "VtArray<GfVec4f>", "vertex", 24)
591 self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "vertex", 24)
592 self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "vertex", 24)
593 self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "vertex", 24)
594 self.check_primvar_missing(prim, "p_mat4x4")
596 self.check_primvar(prim, "sp_bool", "VtArray<bool>", "uniform", 2)
597 self.check_primvar(prim, "sp_int8", "VtArray<unsigned char>", "uniform", 2)
598 self.check_primvar(prim, "sp_int32", "VtArray<int>", "uniform", 2)
599 self.check_primvar(prim, "sp_float", "VtArray<float>", "uniform", 2)
600 self.check_primvar(prim, "sp_color", "VtArray<GfVec4f>", "uniform", 2)
601 self.check_primvar(prim, "sp_byte_color", "VtArray<GfVec4f>", "uniform", 2)
602 self.check_primvar(prim, "sp_vec2", "VtArray<GfVec2f>", "uniform", 2)
603 self.check_primvar(prim, "sp_vec3", "VtArray<GfVec3f>", "uniform", 2)
604 self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "uniform", 2)
605 self.check_primvar_missing(prim, "sp_mat4x4")
607 prim = stage.GetPrimAtPath("/root/Curve_bezier_base/Curves_bezier/Curves")
609 self.check_primvar(prim, "p_bool", "VtArray<bool>", "varying", 10)
610 self.check_primvar(prim, "p_int8", "VtArray<unsigned char>", "varying", 10)
611 self.check_primvar(prim, "p_int32", "VtArray<int>", "varying", 10)
612 self.check_primvar(prim, "p_float", "VtArray<float>", "varying", 10)
613 self.check_primvar(prim, "p_color", "VtArray<GfVec4f>", "varying", 10)
614 self.check_primvar(prim, "p_byte_color", "VtArray<GfVec4f>", "varying", 10)
615 self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "varying", 10)
616 self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "varying", 10)
617 self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "varying", 10)
618 self.check_primvar_missing(prim, "p_mat4x4")
620 self.check_primvar(prim, "sp_bool", "VtArray<bool>", "uniform", 3)
621 self.check_primvar(prim, "sp_int8", "VtArray<unsigned char>", "uniform", 3)
622 self.check_primvar(prim, "sp_int32", "VtArray<int>", "uniform", 3)
623 self.check_primvar(prim, "sp_float", "VtArray<float>", "uniform", 3)
624 self.check_primvar(prim, "sp_color", "VtArray<GfVec4f>", "uniform", 3)
625 self.check_primvar(prim, "sp_byte_color", "VtArray<GfVec4f>", "uniform", 3)
626 self.check_primvar(prim, "sp_vec2", "VtArray<GfVec2f>", "uniform", 3)
627 self.check_primvar(prim, "sp_vec3", "VtArray<GfVec3f>", "uniform", 3)
628 self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "uniform", 3)
629 self.check_primvar_missing(prim, "sp_mat4x4")
631 def test_export_attributes_varying(self):
632 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_varying_test.blend"))
633 # Ensure the simulation zone data is baked for all relevant frames...
634 for frame in range(1, 16):
635 bpy.context.scene.frame_set(frame)
636 bpy.context.scene.frame_set(1)
638 export_path = self.tempdir / "usd_attribute_varying_test.usda"
639 self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
641 stage = Usd.Stage.Open(str(export_path))
642 sparse_frames = [4.0, 5.0, 8.0, 9.0, 12.0, 13.0]
645 # Validate Mesh data
647 mesh1 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh1/mesh1"))
648 mesh2 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh2/mesh2"))
649 mesh3 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh3/mesh3"))
651 # Positions (should be sparsely written)
652 self.assertEqual(mesh1.GetPointsAttr().GetTimeSamples(), sparse_frames)
653 self.assertEqual(mesh2.GetPointsAttr().GetTimeSamples(), [])
654 self.assertEqual(mesh3.GetPointsAttr().GetTimeSamples(), [])
655 # Velocity (should be sparsely written)
656 self.assertEqual(mesh1.GetVelocitiesAttr().GetTimeSamples(), [])
657 self.assertEqual(mesh2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
658 self.assertEqual(mesh3.GetVelocitiesAttr().GetTimeSamples(), [])
659 # Regular primvar (should be sparsely written)
660 self.assertEqual(UsdGeom.PrimvarsAPI(mesh1).GetPrimvar("test").GetTimeSamples(), [])
661 self.assertEqual(UsdGeom.PrimvarsAPI(mesh2).GetPrimvar("test").GetTimeSamples(), [])
662 self.assertEqual(UsdGeom.PrimvarsAPI(mesh3).GetPrimvar("test").GetTimeSamples(), sparse_frames)
663 # Extents of the mesh (should be sparsely written)
664 self.assertEqual(UsdGeom.Boundable(mesh1).GetExtentAttr().GetTimeSamples(), sparse_frames)
665 self.assertEqual(UsdGeom.Boundable(mesh2).GetExtentAttr().GetTimeSamples(), [])
666 self.assertEqual(UsdGeom.Boundable(mesh3).GetExtentAttr().GetTimeSamples(), [])
669 # Validate PointCloud data
671 points1 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud1/PointCloud"))
672 points2 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud2/PointCloud"))
673 points3 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud3/PointCloud"))
674 points4 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud4/PointCloud"))
676 # Positions (should be sparsely written)
677 self.assertEqual(points1.GetPointsAttr().GetTimeSamples(), sparse_frames)
678 self.assertEqual(points2.GetPointsAttr().GetTimeSamples(), [])
679 self.assertEqual(points3.GetPointsAttr().GetTimeSamples(), [])
680 self.assertEqual(points4.GetPointsAttr().GetTimeSamples(), [])
681 # Velocity (should be sparsely written)
682 self.assertEqual(points1.GetVelocitiesAttr().GetTimeSamples(), [])
683 self.assertEqual(points2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
684 self.assertEqual(points3.GetVelocitiesAttr().GetTimeSamples(), [])
685 self.assertEqual(points4.GetVelocitiesAttr().GetTimeSamples(), [])
686 # Radius (should be sparsely written)
687 self.assertEqual(points1.GetWidthsAttr().GetTimeSamples(), [])
688 self.assertEqual(points2.GetWidthsAttr().GetTimeSamples(), [])
689 self.assertEqual(points3.GetWidthsAttr().GetTimeSamples(), sparse_frames)
690 self.assertEqual(points4.GetWidthsAttr().GetTimeSamples(), [])
691 # Regular primvar (should be sparsely written)
692 self.assertEqual(UsdGeom.PrimvarsAPI(points1).GetPrimvar("test").GetTimeSamples(), [])
693 self.assertEqual(UsdGeom.PrimvarsAPI(points2).GetPrimvar("test").GetTimeSamples(), [])
694 self.assertEqual(UsdGeom.PrimvarsAPI(points3).GetPrimvar("test").GetTimeSamples(), [])
695 self.assertEqual(UsdGeom.PrimvarsAPI(points4).GetPrimvar("test").GetTimeSamples(), sparse_frames)
696 # Extents of the point cloud (should be sparsely written)
697 self.assertEqual(UsdGeom.Boundable(points1).GetExtentAttr().GetTimeSamples(), sparse_frames)
698 self.assertEqual(UsdGeom.Boundable(points2).GetExtentAttr().GetTimeSamples(), [])
699 self.assertEqual(UsdGeom.Boundable(points3).GetExtentAttr().GetTimeSamples(), sparse_frames)
700 self.assertEqual(UsdGeom.Boundable(points4).GetExtentAttr().GetTimeSamples(), [])
703 # Validate BasisCurve data
705 curves1 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane1/curves1/Curves"))
706 curves2 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane2/curves2/Curves"))
707 curves3 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane3/curves3/Curves"))
708 curves4 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane4/curves4/Curves"))
710 # Positions (should be sparsely written)
711 self.assertEqual(curves1.GetPointsAttr().GetTimeSamples(), sparse_frames)
712 self.assertEqual(curves2.GetPointsAttr().GetTimeSamples(), [])
713 self.assertEqual(curves3.GetPointsAttr().GetTimeSamples(), [])
714 self.assertEqual(curves4.GetPointsAttr().GetTimeSamples(), [])
715 # Velocity (should be sparsely written)
716 self.assertEqual(curves1.GetVelocitiesAttr().GetTimeSamples(), [])
717 self.assertEqual(curves2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
718 self.assertEqual(curves3.GetVelocitiesAttr().GetTimeSamples(), [])
719 self.assertEqual(curves4.GetVelocitiesAttr().GetTimeSamples(), [])
720 # Radius (should be sparsely written)
721 self.assertEqual(curves1.GetWidthsAttr().GetTimeSamples(), [])
722 self.assertEqual(curves2.GetWidthsAttr().GetTimeSamples(), [])
723 self.assertEqual(curves3.GetWidthsAttr().GetTimeSamples(), sparse_frames)
724 self.assertEqual(curves4.GetWidthsAttr().GetTimeSamples(), [])
725 # Regular primvar (should be sparsely written)
726 self.assertEqual(UsdGeom.PrimvarsAPI(curves1).GetPrimvar("test").GetTimeSamples(), [])
727 self.assertEqual(UsdGeom.PrimvarsAPI(curves2).GetPrimvar("test").GetTimeSamples(), [])
728 self.assertEqual(UsdGeom.PrimvarsAPI(curves3).GetPrimvar("test").GetTimeSamples(), [])
729 self.assertEqual(UsdGeom.PrimvarsAPI(curves4).GetPrimvar("test").GetTimeSamples(), sparse_frames)
731 def test_export_mesh_subd(self):
732 """Test exporting Subdivision Surface attributes and values"""
733 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_mesh_subd.blend"))
734 export_path = self.tempdir / "usd_mesh_subd.usda"
735 self.export_and_validate(
736 filepath=str(export_path),
737 export_subdivision='BEST_MATCH',
738 evaluation_mode="RENDER",
741 stage = Usd.Stage.Open(str(export_path))
743 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_none_boundary_smooth_all/mesh1"))
744 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
745 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'all')
746 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
748 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_corners_boundary_smooth_all/mesh2"))
749 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
750 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'cornersOnly')
751 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
753 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_corners_junctions_boundary_smooth_all/mesh3"))
754 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
755 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'cornersPlus1')
756 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
758 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_corners_junctions_concave_boundary_smooth_all/mesh4"))
759 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
760 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'cornersPlus2')
761 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
763 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_boundaries_boundary_smooth_all/mesh5"))
764 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
765 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
766 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
768 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_all_boundary_smooth_all/mesh6"))
769 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
770 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'none')
771 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
773 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_boundaries_boundary_smooth_keep/mesh7"))
774 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
775 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
776 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeAndCorner')
778 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/crease_verts/crease_verts"))
779 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
780 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
781 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
782 self.assertEqual(len(mesh.GetCornerIndicesAttr().Get()), 7)
783 usd_vert_sharpness = mesh.GetCornerSharpnessesAttr().Get()
784 self.assertEqual(len(usd_vert_sharpness), 7)
785 # A 1.0 crease is INFINITE (10) in USD
786 self.assertAlmostEqual(min(usd_vert_sharpness), 0.1, 5)
787 self.assertEqual(len([sharp for sharp in usd_vert_sharpness if sharp < 1]), 6)
788 self.assertEqual(len([sharp for sharp in usd_vert_sharpness if sharp == 10]), 1)
790 mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/crease_edge/crease_edge"))
791 self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
792 self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
793 self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
794 self.assertEqual(len(mesh.GetCreaseIndicesAttr().Get()), 20)
795 usd_crease_lengths = mesh.GetCreaseLengthsAttr().Get()
796 self.assertEqual(len(usd_crease_lengths), 10)
797 self.assertTrue(all([length == 2 for length in usd_crease_lengths]))
798 usd_crease_sharpness = mesh.GetCreaseSharpnessesAttr().Get()
799 self.assertEqual(len(usd_crease_sharpness), 10)
800 # A 1.0 crease is INFINITE (10) in USD
801 self.assertAlmostEqual(min(usd_crease_sharpness), 0.1, 5)
802 self.assertEqual(len([sharp for sharp in usd_crease_sharpness if sharp < 1]), 9)
803 self.assertEqual(len([sharp for sharp in usd_crease_sharpness if sharp == 10]), 1)
805 def test_export_mesh_triangulate(self):
806 """Test exporting with different triangulation options for meshes."""
808 # Use the current scene to create simple geometry to triangulate
809 bpy.ops.mesh.primitive_plane_add(size=1)
810 bpy.ops.mesh.primitive_circle_add(fill_type='NGON', radius=1, vertices=7)
812 # We assume that triangulation is thoroughly tested elsewhere. Here we are only interested
813 # in checking that USD passes its operator properties through correctly. We use a minimal
814 # combination of quad and ngon methods to test.
815 tri_export_path1 = self.tempdir / "usd_mesh_tri_setup1.usda"
816 self.export_and_validate(
817 filepath=str(tri_export_path1),
818 triangulate_meshes=True,
819 quad_method='FIXED',
820 ngon_method='BEAUTY',
821 evaluation_mode="RENDER",
824 tri_export_path2 = self.tempdir / "usd_mesh_tri_setup2.usda"
825 self.export_and_validate(
826 filepath=str(tri_export_path2),
827 triangulate_meshes=True,
828 quad_method='FIXED_ALTERNATE',
829 ngon_method='CLIP',
830 evaluation_mode="RENDER",
833 stage1 = Usd.Stage.Open(str(tri_export_path1))
834 stage2 = Usd.Stage.Open(str(tri_export_path2))
836 # The Plane should have different vertex ordering because of the quad methods chosen
837 plane1 = UsdGeom.Mesh(stage1.GetPrimAtPath("/root/Plane/Plane"))
838 plane2 = UsdGeom.Mesh(stage2.GetPrimAtPath("/root/Plane/Plane"))
839 indices1 = plane1.GetFaceVertexIndicesAttr().Get()
840 indices2 = plane2.GetFaceVertexIndicesAttr().Get()
841 self.assertEqual(len(indices1), 6)
842 self.assertEqual(len(indices2), 6)
843 self.assertNotEqual(indices1, indices2)
845 # The Circle should have different vertex ordering because of the ngon methods chosen
846 circle1 = UsdGeom.Mesh(stage1.GetPrimAtPath("/root/Circle/Circle"))
847 circle2 = UsdGeom.Mesh(stage2.GetPrimAtPath("/root/Circle/Circle"))
848 indices1 = circle1.GetFaceVertexIndicesAttr().Get()
849 indices2 = circle2.GetFaceVertexIndicesAttr().Get()
850 self.assertEqual(len(indices1), 15)
851 self.assertEqual(len(indices2), 15)
852 self.assertNotEqual(indices1, indices2)
854 def test_export_curves(self):
855 """Test exporting Curve types"""
856 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_curves_test.blend"))
857 export_path = self.tempdir / "usd_curves_test.usda"
858 self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER")
860 stage = Usd.Stage.Open(str(export_path))
862 def check_basis_curve(prim, basis, curve_type, wrap, vert_counts, extent):
863 self.assertEqual(prim.GetBasisAttr().Get(), basis)
864 self.assertEqual(prim.GetTypeAttr().Get(), curve_type)
865 self.assertEqual(prim.GetWrapAttr().Get(), wrap)
866 self.assertEqual(prim.GetWidthsInterpolation(), "varying" if basis == "bezier" else "vertex")
867 self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts)
868 usd_extent = prim.GetExtentAttr().Get()
869 self.assertEqual(self.round_vector(usd_extent[0]), extent[0])
870 self.assertEqual(self.round_vector(usd_extent[1]), extent[1])
872 def check_nurbs_curve(prim, cyclic, orders, vert_counts, knots_count, extent):
873 self.assertEqual(prim.GetOrderAttr().Get(), orders)
874 self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts)
875 self.assertEqual(prim.GetWidthsInterpolation(), "vertex")
876 knots = prim.GetKnotsAttr().Get()
877 usd_extent = prim.GetExtentAttr().Get()
878 self.assertEqual(self.round_vector(usd_extent[0]), extent[0])
879 self.assertEqual(self.round_vector(usd_extent[1]), extent[1])
881 curve_count = len(vert_counts)
882 self.assertEqual(len(knots), knots_count * curve_count)
883 if not cyclic:
884 for i in range(0, curve_count):
885 zeroth_knot = i * len(knots) // curve_count
886 self.assertEqual(knots[zeroth_knot], knots[zeroth_knot + 1], "Knots start rule violated")
887 self.assertEqual(
888 knots[zeroth_knot + knots_count - 1],
889 knots[zeroth_knot + knots_count - 2],
890 "Knots end rule violated")
891 else:
892 self.assertEqual(curve_count, 1, "Validation is only correct for 1 cyclic curve currently")
893 self.assertEqual(
894 knots[0], knots[1] - (knots[knots_count - 2] - knots[knots_count - 3]), "Knots rule violated")
895 self.assertEqual(
896 knots[knots_count - 1], knots[knots_count - 2] + (knots[2] - knots[1]), "Knots rule violated")
898 # Contains 3 CatmullRom curves
899 curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/Cube/Curves/Curves"))
900 check_basis_curve(
901 curve, "catmullRom", "cubic", "pinned", [8, 8, 8], [[-0.3784, -0.0866, 1], [0.2714, -0.0488, 1.3]])
903 # Contains 1 Bezier curve
904 curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCurve/BezierCurve"))
905 check_basis_curve(curve, "bezier", "cubic", "nonperiodic", [7], [[-2.644, -0.0777, 0], [1, 0.9815, 0]])
907 # Contains 1 Bezier curve
908 curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCircle/BezierCircle"))
909 check_basis_curve(curve, "bezier", "cubic", "periodic", [12], [[-1, -1, 0], [1, 1, 0]])
911 # Contains 2 NURBS curves
912 curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCurve/NurbsCurve"))
913 check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-0.75, -1.6898, -0.0117], [2.0896, 0.9583, 0.0293]])
915 # Contains 1 NURBS curve
916 curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCircle/NurbsCircle"))
917 check_nurbs_curve(curve, True, [3], [8], 13, [[-1, -1, 0], [1, 1, 0]])
919 def test_export_animation(self):
920 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend"))
921 export_path = self.tempdir / "usd_anim_test.usda"
922 self.export_and_validate(
923 filepath=str(export_path),
924 export_animation=True,
925 evaluation_mode="RENDER",
928 stage = Usd.Stage.Open(str(export_path))
930 # Validate the simple object animation
931 prim = stage.GetPrimAtPath("/root/cube_anim_xform")
932 self.assertEqual(prim.GetTypeName(), "Xform")
933 loc_samples = UsdGeom.Xformable(prim).GetTranslateOp().GetTimeSamples()
934 rot_samples = UsdGeom.Xformable(prim).GetRotateXYZOp().GetTimeSamples()
935 scale_samples = UsdGeom.Xformable(prim).GetScaleOp().GetTimeSamples()
936 self.assertEqual(loc_samples, [1.0, 2.0, 3.0, 4.0])
937 self.assertEqual(rot_samples, [1.0])
938 self.assertEqual(scale_samples, [1.0])
940 prim = stage.GetPrimAtPath("/root/cube_anim_xform/cube_anim_child")
941 self.assertEqual(prim.GetTypeName(), "Xform")
942 loc_samples = UsdGeom.Xformable(prim).GetTranslateOp().GetTimeSamples()
943 rot_samples = UsdGeom.Xformable(prim).GetRotateXYZOp().GetTimeSamples()
944 scale_samples = UsdGeom.Xformable(prim).GetScaleOp().GetTimeSamples()
945 self.assertEqual(loc_samples, [1.0])
946 self.assertEqual(rot_samples, [1.0, 2.0, 3.0, 4.0])
947 self.assertEqual(scale_samples, [1.0])
949 # Validate the armature animation
950 prim = stage.GetPrimAtPath("/root/Armature/Armature")
951 self.assertEqual(prim.GetTypeName(), "Skeleton")
952 prim_skel = UsdSkel.BindingAPI(prim)
953 anim = UsdSkel.Animation(prim_skel.GetAnimationSource())
954 self.assertEqual(anim.GetPrim().GetName(), "ArmatureAction_001")
955 self.assertEqual(anim.GetJointsAttr().Get(),
956 ['Bone',
957 'Bone/Bone_001',
958 'Bone/Bone_001/Bone_002',
959 'Bone/Bone_001/Bone_002/Bone_003',
960 'Bone/Bone_001/Bone_002/Bone_003/Bone_004'])
961 loc_samples = anim.GetTranslationsAttr().GetTimeSamples()
962 rot_samples = anim.GetRotationsAttr().GetTimeSamples()
963 scale_samples = anim.GetScalesAttr().GetTimeSamples()
964 self.assertEqual(loc_samples, [])
965 self.assertEqual(rot_samples, [1.0, 2.0, 3.0])
966 self.assertEqual(scale_samples, [])
968 # Validate the shape key animation
969 prim = stage.GetPrimAtPath("/root/cube_anim_keys")
970 self.assertEqual(prim.GetTypeName(), "SkelRoot")
971 prim_skel = UsdSkel.BindingAPI(prim.GetPrimAtPath("cube_anim_keys"))
972 self.assertEqual(prim_skel.GetBlendShapesAttr().Get(), ['Key_1'])
973 prim_skel = UsdSkel.BindingAPI(prim.GetPrimAtPath("Skel"))
974 anim = UsdSkel.Animation(prim_skel.GetAnimationSource())
975 weight_samples = anim.GetBlendShapeWeightsAttr().GetTimeSamples()
976 self.assertEqual(weight_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
978 def test_export_volumes(self):
979 """Test various combinations of volume export including with all supported volume modifiers."""
981 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_volumes.blend"))
982 # Ensure the simulation zone data is baked for all relevant frames...
983 for frame in range(4, 15):
984 bpy.context.scene.frame_set(frame)
985 bpy.context.scene.frame_set(4)
987 export_path = self.tempdir / "usd_volumes.usda"
988 self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
990 stage = Usd.Stage.Open(str(export_path))
992 # Validate that we see some form of time varyability across the Volume prim's extents and
993 # file paths. The data should be sparse so it should only be written on the frames which
994 # change.
996 # File sequence
997 vol_fileseq = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence"))
998 density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence/density_noise"))
999 flame = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence/flame_noise"))
1000 self.assertEqual(vol_fileseq.GetExtentAttr().GetTimeSamples(), [10.0, 11.0])
1001 self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
1002 self.assertEqual(density.GetFilePathAttr().GetTimeSamples(), [8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
1003 self.assertEqual(flame.GetFieldNameAttr().GetTimeSamples(), [])
1004 self.assertEqual(flame.GetFilePathAttr().GetTimeSamples(), [8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
1006 # Mesh To Volume
1007 vol_mesh2vol = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_mesh2vol/vol_mesh2vol"))
1008 density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_mesh2vol/vol_mesh2vol/density"))
1009 self.assertEqual(vol_mesh2vol.GetExtentAttr().GetTimeSamples(),
1010 [6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
1011 self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
1012 self.assertEqual(density.GetFilePathAttr().GetTimeSamples(),
1013 [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
1015 # Volume Displace
1016 vol_displace = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_displace/vol_displace"))
1017 unnamed = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_displace/vol_displace/_"))
1018 self.assertEqual(vol_displace.GetExtentAttr().GetTimeSamples(),
1019 [5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
1020 self.assertEqual(unnamed.GetFieldNameAttr().GetTimeSamples(), [])
1021 self.assertEqual(unnamed.GetFilePathAttr().GetTimeSamples(),
1022 [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
1024 # Geometry Node simulation
1025 vol_sim = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_sim/Volume"))
1026 density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_sim/Volume/density"))
1027 self.assertEqual(vol_sim.GetExtentAttr().GetTimeSamples(),
1028 [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
1029 self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
1030 self.assertEqual(density.GetFilePathAttr().GetTimeSamples(),
1031 [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
1033 def test_export_xform_ops(self):
1034 """Test exporting different xform operation modes."""
1036 # Create a simple scene and export using each of our xform op modes
1037 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
1038 loc = [1, 2, 3]
1039 rot = [math.pi / 4, 0, math.pi / 8]
1040 scale = [1, 2, 3]
1042 bpy.ops.mesh.primitive_plane_add(location=loc, rotation=rot)
1043 bpy.data.objects[0].scale = scale
1045 test_path1 = self.tempdir / "temp_xform_trs_test.usda"
1046 self.export_and_validate(filepath=str(test_path1), xform_op_mode='TRS')
1048 test_path2 = self.tempdir / "temp_xform_tos_test.usda"
1049 self.export_and_validate(filepath=str(test_path2), xform_op_mode='TOS')
1051 test_path3 = self.tempdir / "temp_xform_mat_test.usda"
1052 self.export_and_validate(filepath=str(test_path3), xform_op_mode='MAT')
1054 # Validate relevant details for each case
1055 stage = Usd.Stage.Open(str(test_path1))
1056 xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
1057 rot_degs = [math.degrees(rot[0]), math.degrees(rot[1]), math.degrees(rot[2])]
1058 self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:translate', 'xformOp:rotateXYZ', 'xformOp:scale'])
1059 self.assertEqual(self.round_vector(xf.GetTranslateOp().Get()), loc)
1060 self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), rot_degs)
1061 self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
1063 stage = Usd.Stage.Open(str(test_path2))
1064 xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
1065 orient_quat = xf.GetOrientOp().Get()
1066 self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:translate', 'xformOp:orient', 'xformOp:scale'])
1067 self.assertEqual(self.round_vector(xf.GetTranslateOp().Get()), loc)
1068 self.assertEqual(round(orient_quat.GetReal(), 4), 0.9061)
1069 self.assertEqual(self.round_vector(orient_quat.GetImaginary()), [0.3753, 0.0747, 0.1802])
1070 self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
1072 stage = Usd.Stage.Open(str(test_path3))
1073 xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
1074 mat = xf.GetTransformOp().Get()
1075 mat = [
1076 self.round_vector(mat[0]), self.round_vector(mat[1]), self.round_vector(mat[2]), self.round_vector(mat[3])
1078 expected = [
1079 [0.9239, 0.3827, 0.0, 0.0],
1080 [-0.5412, 1.3066, 1.4142, 0.0],
1081 [0.8118, -1.9598, 2.1213, 0.0],
1082 [1.0, 2.0, 3.0, 1.0]
1084 self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:transform'])
1085 self.assertEqual(mat, expected)
1087 def test_export_orientation(self):
1088 """Test exporting different orientation configurations."""
1090 # Using the empty scene is fine for this
1091 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
1093 test_path1 = self.tempdir / "temp_orientation_yup.usda"
1094 self.export_and_validate(
1095 filepath=str(test_path1),
1096 convert_orientation=True,
1097 export_global_forward_selection='NEGATIVE_Z',
1098 export_global_up_selection='Y')
1100 test_path2 = self.tempdir / "temp_orientation_zup_rev.usda"
1101 self.export_and_validate(
1102 filepath=str(test_path2),
1103 convert_orientation=True,
1104 export_global_forward_selection='NEGATIVE_Y',
1105 export_global_up_selection='Z')
1107 stage = Usd.Stage.Open(str(test_path1))
1108 xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
1109 self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [-90, 0, 0])
1111 stage = Usd.Stage.Open(str(test_path2))
1112 xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
1113 self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [0, 0, 180])
1115 def test_materialx_network(self):
1116 """Test exporting that a MaterialX export makes it out alright"""
1117 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
1118 export_path = self.tempdir / "materialx.usda"
1120 # USD currently has an issue where embedded MaterialX graphs cause validation to fail.
1121 # Skip validation and just run a regular export until this is fixed.
1122 # See: https://github.com/PixarAnimationStudios/OpenUSD/pull/3243
1123 res = bpy.ops.wm.usd_export(
1124 filepath=str(export_path),
1125 export_materials=True,
1126 generate_materialx_network=True,
1127 evaluation_mode="RENDER",
1129 self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
1131 stage = Usd.Stage.Open(str(export_path))
1132 material_prim = stage.GetPrimAtPath("/root/_materials/Material")
1133 self.assertTrue(material_prim, "Could not find Material prim")
1135 material = UsdShade.Material(material_prim)
1136 mtlx_output = material.GetOutput("mtlx:surface")
1137 self.assertTrue(mtlx_output, "Could not find mtlx output")
1139 connection, source_name, _ = UsdShade.ConnectableAPI.GetConnectedSource(
1140 mtlx_output
1141 ) or [None, None, None]
1143 self.assertTrue((connection and source_name), "Could not find mtlx output source")
1145 shader = UsdShade.Shader(connection.GetPrim())
1146 self.assertTrue(shader, "Connected prim is not a shader")
1148 shader_id = shader.GetIdAttr().Get()
1149 self.assertEqual(shader_id, "ND_standard_surface_surfaceshader", "Shader is not a Standard Surface")
1151 def test_hooks(self):
1152 """Validate USD Hook integration for both import and export"""
1154 # Create a simple scene with 1 object and 1 material
1155 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
1156 material = bpy.data.materials.new(name="test_material")
1157 material.use_nodes = True
1158 bpy.ops.mesh.primitive_plane_add()
1159 bpy.data.objects[0].data.materials.append(material)
1161 # Register both USD hooks
1162 bpy.utils.register_class(USDHook1)
1163 bpy.utils.register_class(USDHook2)
1165 # Instruct them to do various actions inside their implementation
1166 USDHookBase.instructions = {
1167 "on_material_export": ["return False", "return True"],
1168 "on_export": ["throw", "return True"],
1169 "on_import": ["throw", "return True"],
1172 USDHookBase.responses = {
1173 "on_material_export": [],
1174 "on_export": [],
1175 "on_import": [],
1178 test_path = self.tempdir / "hook.usda"
1180 try:
1181 self.export_and_validate(filepath=str(test_path))
1182 except:
1183 pass
1185 try:
1186 bpy.ops.wm.usd_import(filepath=str(test_path))
1187 except:
1188 pass
1190 # Unregister the hooks. We do this here in case the following asserts fail.
1191 bpy.utils.unregister_class(USDHook1)
1192 bpy.utils.unregister_class(USDHook2)
1194 # Validate that the Hooks executed and responded accordingly...
1195 self.assertEqual(USDHookBase.responses["on_material_export"], ["returned False", "returned True"])
1196 self.assertEqual(USDHookBase.responses["on_export"], ["threw exception", "returned True"])
1197 self.assertEqual(USDHookBase.responses["on_import"], ["threw exception", "returned True"])
1199 # Now that the hooks are unregistered they should not be executed for import and export.
1200 USDHookBase.responses = {
1201 "on_material_export": [],
1202 "on_export": [],
1203 "on_import": [],
1205 self.export_and_validate(filepath=str(test_path))
1206 self.export_and_validate(filepath=str(test_path))
1207 self.assertEqual(USDHookBase.responses["on_material_export"], [])
1208 self.assertEqual(USDHookBase.responses["on_export"], [])
1209 self.assertEqual(USDHookBase.responses["on_import"], [])
1211 def test_merge_parent_xform_false(self):
1212 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_export_test.blend"))
1214 test_path = self.tempdir / "test_merge_parent_xform_false.usda"
1216 self.export_and_validate(filepath=str(test_path), merge_parent_xform=False)
1218 expected = (
1219 ("/root", "Xform"),
1220 ("/root/Dupli1", "Xform"),
1221 ("/root/Dupli1/GEO_Head_0", "Xform"),
1222 ("/root/Dupli1/GEO_Head_0/Face", "Mesh"),
1223 ("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2", "Xform"),
1224 ("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2/Ear", "Mesh"),
1225 ("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1", "Xform"),
1226 ("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1/Ear", "Mesh"),
1227 ("/root/Dupli1/GEO_Head_0/GEO_Nose_3", "Xform"),
1228 ("/root/Dupli1/GEO_Head_0/GEO_Nose_3/Nose", "Mesh"),
1229 ("/root/_materials", "Scope"),
1230 ("/root/_materials/Head", "Material"),
1231 ("/root/_materials/Head/Principled_BSDF", "Shader"),
1232 ("/root/_materials/Nose", "Material"),
1233 ("/root/_materials/Nose/Principled_BSDF", "Shader"),
1234 ("/root/ParentOfDupli2", "Xform"),
1235 ("/root/ParentOfDupli2/Icosphere", "Mesh"),
1236 ("/root/ParentOfDupli2/Dupli2", "Xform"),
1237 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0", "Xform"),
1238 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/Face", "Mesh"),
1239 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1", "Xform"),
1240 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1/Ear", "Mesh"),
1241 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2", "Xform"),
1242 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2/Ear", "Mesh"),
1243 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3", "Xform"),
1244 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3/Nose", "Mesh"),
1245 ("/root/Ground_plane", "Xform"),
1246 ("/root/Ground_plane/Plane", "Mesh"),
1247 ("/root/Ground_plane/OutsideDupliGrandParent", "Xform"),
1248 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent", "Xform"),
1249 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", "Xform"),
1250 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", "Mesh"),
1251 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", "Xform"),
1252 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear", "Mesh"),
1253 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", "Xform"),
1254 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose", "Mesh"),
1255 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", "Xform"),
1256 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear", "Mesh"),
1257 ("/root/Camera", "Xform"),
1258 ("/root/Camera/Camera", "Camera"),
1259 ("/root/env_light", "DomeLight")
1262 def key(el):
1263 return el[0]
1265 expected = tuple(sorted(expected, key=key))
1267 stage = Usd.Stage.Open(str(test_path))
1268 actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
1269 actual = tuple(sorted(actual, key=key))
1271 self.assertTupleEqual(expected, actual)
1273 def test_merge_parent_xform_true(self):
1274 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_export_test.blend"))
1276 test_path = self.tempdir / "test_merge_parent_xform_true.usda"
1278 self.export_and_validate(filepath=str(test_path), merge_parent_xform=True)
1280 expected = (
1281 ("/root", "Xform"),
1282 ("/root/Dupli1", "Xform"),
1283 ("/root/Dupli1/GEO_Head_0", "Xform"),
1284 ("/root/Dupli1/GEO_Head_0/Face", "Mesh"),
1285 ("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2", "Mesh"),
1286 ("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1", "Mesh"),
1287 ("/root/Dupli1/GEO_Head_0/GEO_Nose_3", "Mesh"),
1288 ("/root/_materials", "Scope"),
1289 ("/root/_materials/Head", "Material"),
1290 ("/root/_materials/Head/Principled_BSDF", "Shader"),
1291 ("/root/_materials/Nose", "Material"),
1292 ("/root/_materials/Nose/Principled_BSDF", "Shader"),
1293 ("/root/ParentOfDupli2", "Xform"),
1294 ("/root/ParentOfDupli2/Icosphere", "Mesh"),
1295 ("/root/ParentOfDupli2/Dupli2", "Xform"),
1296 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0", "Xform"),
1297 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/Face", "Mesh"),
1298 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1", "Mesh"),
1299 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2", "Mesh"),
1300 ("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3", "Mesh"),
1301 ("/root/Ground_plane", "Xform"),
1302 ("/root/Ground_plane/Plane", "Mesh"),
1303 ("/root/Ground_plane/OutsideDupliGrandParent", "Xform"),
1304 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent", "Xform"),
1305 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", "Xform"),
1306 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", "Mesh"),
1307 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", "Mesh"),
1308 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", "Mesh"),
1309 ("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", "Mesh"),
1310 ("/root/Camera", "Camera"),
1311 ("/root/env_light", "DomeLight")
1314 def key(el):
1315 return el[0]
1317 expected = tuple(sorted(expected, key=key))
1319 stage = Usd.Stage.Open(str(test_path))
1320 actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
1321 actual = tuple(sorted(actual, key=key))
1323 self.assertTupleEqual(expected, actual)
1325 def test_export_units(self):
1326 """Test specifying stage meters per unit on export."""
1327 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
1329 units = (
1330 ("mm", 'MILLIMETERS', 0.001), ("cm", 'CENTIMETERS', 0.01), ("km", 'KILOMETERS', 1000),
1331 ("in", 'INCHES', 0.0254), ("ft", 'FEET', 0.3048), ("yd", 'YARDS', 0.9144),
1332 ("default", "", 1), ("custom", 'CUSTOM', 0.125)
1334 for name, unit, value in units:
1335 export_path = self.tempdir / f"usd_export_units_test_{name}.usda"
1336 if name == "default":
1337 self.export_and_validate(filepath=str(export_path))
1338 elif name == "custom":
1339 self.export_and_validate(filepath=str(export_path), convert_scene_units=unit, meters_per_unit=value)
1340 else:
1341 self.export_and_validate(filepath=str(export_path), convert_scene_units=unit)
1343 # Verify that meters per unit were set correctly
1344 stage = Usd.Stage.Open(str(export_path))
1345 self.assertEqual(UsdGeom.GetStageMetersPerUnit(stage), value)
1347 def test_export_native_instancing_true(self):
1348 """Test exporting instanced objects to native (scne graph) instances."""
1349 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "nested_instancing_test.blend"))
1351 export_path = self.tempdir / "usd_export_nested_instancing_true.usda"
1352 self.export_and_validate(
1353 filepath=str(export_path),
1354 use_instancing=True
1357 # The USD should contain two instances of a plane which has two
1358 # instances of a point cloud as children.
1359 stage = Usd.Stage.Open(str(export_path))
1361 stats = UsdUtils.ComputeUsdStageStats(stage)
1362 self.assertEqual(stats['totalInstanceCount'], 6, "Unexpected number of instances")
1363 self.assertEqual(stats['prototypeCount'], 2, "Unexpected number of prototypes")
1364 self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 1, "Unexpected number of primary meshes")
1365 self.assertEqual(stats['primary']['primCountsByType']['Points'], 1, "Unexpected number of primary point clouds")
1366 self.assertEqual(stats['prototypes']['primCountsByType']['Mesh'], 1, "Unexpected number of prototype meshes")
1367 self.assertEqual(stats['prototypes']['primCountsByType']['Points'],
1368 1, "Unexpected number of prototype point clouds")
1370 # Get the prototypes root.
1371 protos_root_path = Sdf.Path("/root/prototypes")
1372 prim = stage.GetPrimAtPath(protos_root_path)
1373 assert prim
1374 self.assertTrue(prim.IsAbstract())
1376 # Get the first plane instance.
1377 prim = stage.GetPrimAtPath("/root/plane_001/Plane_0")
1378 assert prim
1379 assert prim.IsInstance()
1381 # Get the second plane instance.
1382 prim = stage.GetPrimAtPath("/root/plane/Plane_0")
1383 assert prim
1384 assert prim.IsInstance()
1386 # Ensure all the prototype paths are under the pototypes root.
1387 for prim in stage.Traverse():
1388 if prim.IsInstance():
1389 arcs = Usd.PrimCompositionQuery.GetDirectReferences(prim).GetCompositionArcs()
1390 for arc in arcs:
1391 target_path = arc.GetTargetPrimPath()
1392 self.assertTrue(target_path.HasPrefix(protos_root_path))
1394 def test_export_native_instancing_false(self):
1395 """Test exporting instanced objects with instancing disabled."""
1396 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "nested_instancing_test.blend"))
1398 export_path = self.tempdir / "usd_export_nested_instancing_false.usda"
1399 self.export_and_validate(
1400 filepath=str(export_path),
1401 use_instancing=False
1404 # The USD should contain no instances.
1405 stage = Usd.Stage.Open(str(export_path))
1407 stats = UsdUtils.ComputeUsdStageStats(stage)
1408 self.assertEqual(stats['totalInstanceCount'], 0, "Unexpected number of instances")
1409 self.assertEqual(stats['prototypeCount'], 0, "Unexpected number of prototypes")
1410 self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 2, "Unexpected number of primary meshes")
1411 self.assertEqual(stats['primary']['primCountsByType']['Points'], 4, "Unexpected number of primary point clouds")
1413 def test_texture_export_hook(self):
1414 """Exporting textures from on_material_export USD hook."""
1416 # Clear USD hook results.
1417 ExportTextureUSDHook.exported_textures = {}
1419 bpy.utils.register_class(ExportTextureUSDHook)
1420 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
1422 export_path = self.tempdir / "usd_materials_export.usda"
1424 self.export_and_validate(
1425 filepath=str(export_path),
1426 export_materials=True,
1427 generate_preview_surface=False,
1430 # Verify that the exported texture paths were returned as expected.
1431 expected = {'/root/_materials/Transforms': './textures/test_grid_<UDIM>.png',
1432 '/root/_materials/Clip_With_Round': './textures/test_grid_<UDIM>.png',
1433 '/root/_materials/NormalMap': './textures/test_normal.exr',
1434 '/root/_materials/Material': './textures/test_grid_<UDIM>.png',
1435 '/root/_materials/Clip_With_LessThanInvert': './textures/test_grid_<UDIM>.png',
1436 '/root/_materials/NormalMap_Scale_Bias': './textures/test_normal_invertY.exr'}
1438 self.assertDictEqual(ExportTextureUSDHook.exported_textures,
1439 expected,
1440 "Unexpected texture export paths")
1442 bpy.utils.unregister_class(ExportTextureUSDHook)
1444 # Verify that the texture files were copied as expected.
1445 tex_names = ['test_grid_1001.png', 'test_grid_1002.png',
1446 'test_normal.exr', 'test_normal_invertY.exr']
1448 for name in tex_names:
1449 tex_path = self.tempdir / "textures" / name
1450 self.assertTrue(tex_path.exists(),
1451 f"Exported texture {tex_path} doesn't exist")
1453 def test_inmem_pack_texture_export_hook(self):
1454 """Exporting packed and in memory textures from on_material_export USD hook."""
1456 # Clear hook results.
1457 ExportTextureUSDHook.exported_textures = {}
1459 bpy.utils.register_class(ExportTextureUSDHook)
1460 bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_inmem_pack.blend"))
1462 export_path = self.tempdir / "usd_materials_inmem_pack.usda"
1464 self.export_and_validate(
1465 filepath=str(export_path),
1466 export_materials=True,
1467 generate_preview_surface=False,
1470 # Verify that the exported texture paths were returned as expected.
1471 expected = {'/root/_materials/MAT_pack_udim': './textures/test_grid_<UDIM>.png',
1472 '/root/_materials/MAT_pack_single': './textures/test_single.png',
1473 '/root/_materials/MAT_inmem_udim': './textures/inmem_udim.<UDIM>.png',
1474 '/root/_materials/MAT_inmem_single': './textures/inmem_single.png'}
1476 self.assertDictEqual(ExportTextureUSDHook.exported_textures,
1477 expected,
1478 "Unexpected texture export paths")
1480 bpy.utils.unregister_class(ExportTextureUSDHook)
1482 # Verify that the texture files were copied as expected.
1483 tex_names = ['test_grid_1001.png', 'test_grid_1002.png',
1484 'test_single.png',
1485 'inmem_udim.1001.png', 'inmem_udim.1002.png',
1486 'inmem_single.png']
1488 for name in tex_names:
1489 tex_path = self.tempdir / "textures" / name
1490 self.assertTrue(tex_path.exists(),
1491 f"Exported texture {tex_path} doesn't exist")
1494 class USDHookBase():
1495 instructions = {}
1496 responses = {}
1498 @staticmethod
1499 def follow_instructions(name, operation):
1500 instruction = USDHookBase.instructions[operation].pop(0)
1501 if instruction == "throw":
1502 USDHookBase.responses[operation].append("threw exception")
1503 raise RuntimeError(f"** {name} failing {operation} **")
1504 elif instruction == "return False":
1505 USDHookBase.responses[operation].append("returned False")
1506 return False
1508 USDHookBase.responses[operation].append("returned True")
1509 return True
1511 @staticmethod
1512 def do_on_export(name, export_context):
1513 stage = export_context.get_stage()
1514 depsgraph = export_context.get_depsgraph()
1515 if not stage.GetDefaultPrim().IsValid():
1516 raise RuntimeError("Unexpected failure: bad stage")
1517 if len(depsgraph.ids) == 0:
1518 raise RuntimeError("Unexpected failure: bad depsgraph")
1520 return USDHookBase.follow_instructions(name, "on_export")
1522 @staticmethod
1523 def do_on_material_export(name, export_context, bl_material, usd_material):
1524 stage = export_context.get_stage()
1525 if stage.expired:
1526 raise RuntimeError("Unexpected failure: bad stage")
1527 if not usd_material.GetPrim().IsValid():
1528 raise RuntimeError("Unexpected failure: bad usd_material")
1529 if bl_material is None:
1530 raise RuntimeError("Unexpected failure: bad bl_material")
1532 return USDHookBase.follow_instructions(name, "on_material_export")
1534 @staticmethod
1535 def do_on_import(name, import_context):
1536 stage = import_context.get_stage()
1537 if not stage.GetDefaultPrim().IsValid():
1538 raise RuntimeError("Unexpected failure: bad stage")
1540 return USDHookBase.follow_instructions(name, "on_import")
1543 class USDHook1(USDHookBase, bpy.types.USDHook):
1544 bl_idname = "usd_hook_1"
1545 bl_label = "Hook 1"
1547 @staticmethod
1548 def on_export(export_context):
1549 return USDHookBase.do_on_export(USDHook1.bl_label, export_context)
1551 @staticmethod
1552 def on_material_export(export_context, bl_material, usd_material):
1553 return USDHookBase.do_on_material_export(USDHook1.bl_label, export_context, bl_material, usd_material)
1555 @staticmethod
1556 def on_import(import_context):
1557 return USDHookBase.do_on_import(USDHook1.bl_label, import_context)
1560 class USDHook2(USDHookBase, bpy.types.USDHook):
1561 bl_idname = "usd_hook_2"
1562 bl_label = "Hook 2"
1564 @staticmethod
1565 def on_export(export_context):
1566 return USDHookBase.do_on_export(USDHook2.bl_label, export_context)
1568 @staticmethod
1569 def on_material_export(export_context, bl_material, usd_material):
1570 return USDHookBase.do_on_material_export(USDHook2.bl_label, export_context, bl_material, usd_material)
1572 @staticmethod
1573 def on_import(import_context):
1574 return USDHookBase.do_on_import(USDHook2.bl_label, import_context)
1577 class ExportTextureUSDHook(bpy.types.USDHook):
1578 bl_idname = "export_texture_usd_hook"
1579 bl_label = "Export Texture USD Hook"
1581 exported_textures = {}
1583 @staticmethod
1584 def on_material_export(export_context, bl_material, usd_material):
1586 If a texture image node exists in the given material's
1587 node tree, call exprt_texture() on the image and cache
1588 the returned path.
1590 tex_image_node = None
1591 if bl_material and bl_material.node_tree:
1592 for node in bl_material.node_tree.nodes:
1593 if node.type == 'TEX_IMAGE':
1594 tex_image_node = node
1596 if tex_image_node is None:
1597 return False
1599 tex_path = export_context.export_texture(tex_image_node.image)
1601 ExportTextureUSDHook.exported_textures[usd_material.GetPath()
1602 .pathString] = tex_path
1604 return True
1607 def main():
1608 global args
1609 import argparse
1611 if "--" in sys.argv:
1612 argv = [sys.argv[0]] + sys.argv[sys.argv.index("--") + 1:]
1613 else:
1614 argv = sys.argv
1616 parser = argparse.ArgumentParser()
1617 parser.add_argument("--testdir", required=True, type=pathlib.Path)
1618 args, remaining = parser.parse_known_args(argv)
1620 unittest.main(argv=remaining, verbosity=0)
1623 if __name__ == "__main__":
1624 main()