1 # SPDX-FileCopyrightText: 2023 Blender Authors
3 # SPDX-License-Identifier: GPL-2.0-or-later
12 from pxr
import Gf
, Sdf
, Usd
, UsdGeom
, UsdShade
, UsdSkel
, UsdUtils
, UsdVol
16 sys
.path
.append(str(pathlib
.Path(__file__
).parent
.absolute()))
17 from modules
.colored_print
import (print_message
, use_message_colors
)
23 class AbstractUSDTest(unittest
.TestCase
):
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:
33 self
.assertTrue(self
.testdir
.exists(), "Test dir {0} should exist".format(self
.testdir
))
34 print_message(self
._testMethodName
, 'SUCCESS', 'RUN')
37 self
._tempdir
.cleanup()
39 result
= self
._outcome
.result
40 ok
= all(test
!= self
for test
, _
in result
.errors
+ result
.failures
)
42 print_message(self
._testMethodName
, 'FAILURE', 'FAILED')
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(
58 skipARKitRootLayerCheck
=False,
59 rootPackageOnly
=False,
63 checker
.CheckCompliance(export_path
)
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
__
76 issues
= rule
.GetFailedChecks() + rule
.GetWarnings() + rule
.GetErrors()
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).
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
):
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()
137 Gf
.Vec3d(extent
[0]), Gf
.Vec3d(-0.7313742, -0.68043584, -0.5801515)
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)
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]
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,
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',
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
)
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")
888 knots
[zeroth_knot
+ knots_count
- 1],
889 knots
[zeroth_knot
+ knots_count
- 2],
890 "Knots end rule violated")
892 self
.assertEqual(curve_count
, 1, "Validation is only correct for 1 cyclic curve currently")
894 knots
[0], knots
[1] - (knots
[knots_count
- 2] - knots
[knots_count
- 3]), "Knots rule violated")
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"))
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(),
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
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])
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])
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"))
1039 rot
= [math
.pi
/ 4, 0, math
.pi
/ 8]
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()
1076 self
.round_vector(mat
[0]), self
.round_vector(mat
[1]), self
.round_vector(mat
[2]), self
.round_vector(mat
[3])
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(
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": [],
1178 test_path
= self
.tempdir
/ "hook.usda"
1181 self
.export_and_validate(filepath
=str(test_path
))
1186 bpy
.ops
.wm
.usd_import(filepath
=str(test_path
))
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": [],
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)
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")
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)
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")
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"))
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
)
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
),
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
)
1374 self
.assertTrue(prim
.IsAbstract())
1376 # Get the first plane instance.
1377 prim
= stage
.GetPrimAtPath("/root/plane_001/Plane_0")
1379 assert prim
.IsInstance()
1381 # Get the second plane instance.
1382 prim
= stage
.GetPrimAtPath("/root/plane/Plane_0")
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()
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
,
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
,
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',
1485 'inmem_udim.1001.png', 'inmem_udim.1002.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():
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")
1508 USDHookBase
.responses
[operation
].append("returned True")
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")
1523 def do_on_material_export(name
, export_context
, bl_material
, usd_material
):
1524 stage
= export_context
.get_stage()
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")
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"
1548 def on_export(export_context
):
1549 return USDHookBase
.do_on_export(USDHook1
.bl_label
, export_context
)
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
)
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"
1565 def on_export(export_context
):
1566 return USDHookBase
.do_on_export(USDHook2
.bl_label
, export_context
)
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
)
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
= {}
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
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:
1599 tex_path
= export_context
.export_texture(tex_image_node
.image
)
1601 ExportTextureUSDHook
.exported_textures
[usd_material
.GetPath()
1602 .pathString
] = tex_path
1611 if "--" in sys
.argv
:
1612 argv
= [sys
.argv
[0]] + sys
.argv
[sys
.argv
.index("--") + 1:]
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__":