2023-08-16 00:20:26 +10:00
|
|
|
# SPDX-FileCopyrightText: 2023 Blender Authors
|
2023-06-15 13:09:04 +10:00
|
|
|
#
|
2023-02-14 12:11:53 +01:00
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
2023-06-15 13:09:04 +10:00
|
|
|
|
2024-08-24 06:58:27 +02:00
|
|
|
import math
|
2025-01-26 04:43:16 +01:00
|
|
|
import os
|
2023-02-14 12:11:53 +01:00
|
|
|
import pathlib
|
|
|
|
import pprint
|
|
|
|
import sys
|
|
|
|
import tempfile
|
|
|
|
import unittest
|
2025-04-30 19:54:17 +02:00
|
|
|
from pxr import Gf, Sdf, Usd, UsdGeom, UsdMtlx, UsdShade, UsdSkel, UsdUtils, UsdVol
|
2023-02-14 12:11:53 +01:00
|
|
|
|
|
|
|
import bpy
|
|
|
|
|
2025-01-26 04:43:16 +01:00
|
|
|
sys.path.append(str(pathlib.Path(__file__).parent.absolute()))
|
|
|
|
from modules.colored_print import (print_message, use_message_colors)
|
|
|
|
|
|
|
|
|
2023-02-14 12:11:53 +01:00
|
|
|
args = None
|
|
|
|
|
2023-02-15 17:39:53 -05:00
|
|
|
|
2023-02-14 12:11:53 +01:00
|
|
|
class AbstractUSDTest(unittest.TestCase):
|
|
|
|
@classmethod
|
|
|
|
def setUpClass(cls):
|
|
|
|
cls._tempdir = tempfile.TemporaryDirectory()
|
|
|
|
cls.testdir = args.testdir
|
|
|
|
cls.tempdir = pathlib.Path(cls._tempdir.name)
|
2025-01-26 04:43:16 +01:00
|
|
|
if os.environ.get("BLENDER_TEST_COLOR") is not None:
|
|
|
|
use_message_colors()
|
2023-02-14 12:11:53 +01:00
|
|
|
|
|
|
|
def setUp(self):
|
2025-01-26 04:43:16 +01:00
|
|
|
self.assertTrue(self.testdir.exists(), "Test dir {0} should exist".format(self.testdir))
|
|
|
|
print_message(self._testMethodName, 'SUCCESS', 'RUN')
|
2023-02-14 12:11:53 +01:00
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
self._tempdir.cleanup()
|
|
|
|
|
2025-01-26 04:43:16 +01:00
|
|
|
result = self._outcome.result
|
|
|
|
ok = all(test != self for test, _ in result.errors + result.failures)
|
|
|
|
if not ok:
|
|
|
|
print_message(self._testMethodName, 'FAILURE', 'FAILED')
|
|
|
|
else:
|
|
|
|
print_message(self._testMethodName, 'SUCCESS', 'PASSED')
|
|
|
|
|
2024-10-04 02:50:16 +02:00
|
|
|
def export_and_validate(self, **kwargs):
|
|
|
|
"""Export and validate the resulting USD file."""
|
2023-02-14 12:11:53 +01:00
|
|
|
|
2024-10-04 02:50:16 +02:00
|
|
|
export_path = kwargs["filepath"]
|
|
|
|
|
|
|
|
# Do the actual export
|
|
|
|
res = bpy.ops.wm.usd_export(**kwargs)
|
2023-02-14 12:11:53 +01:00
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
2024-10-04 02:50:16 +02:00
|
|
|
# Validate resulting file
|
2023-02-14 12:11:53 +01:00
|
|
|
checker = UsdUtils.ComplianceChecker(
|
|
|
|
arkit=False,
|
|
|
|
skipARKitRootLayerCheck=False,
|
|
|
|
rootPackageOnly=False,
|
|
|
|
skipVariants=False,
|
|
|
|
verbose=False,
|
|
|
|
)
|
2024-10-04 02:50:16 +02:00
|
|
|
checker.CheckCompliance(export_path)
|
2023-02-14 12:11:53 +01:00
|
|
|
|
|
|
|
failed_checks = {}
|
|
|
|
|
|
|
|
# The ComplianceChecker does not know how to resolve <UDIM> tags, so
|
|
|
|
# it will flag "textures/test_grid_<UDIM>.png" as a missing reference.
|
|
|
|
# That reference is in fact OK, so we skip the rule for this test.
|
|
|
|
to_skip = ("MissingReferenceChecker",)
|
|
|
|
for rule in checker._rules:
|
|
|
|
name = rule.__class__.__name__
|
|
|
|
if name in to_skip:
|
|
|
|
continue
|
|
|
|
|
|
|
|
issues = rule.GetFailedChecks() + rule.GetWarnings() + rule.GetErrors()
|
|
|
|
if not issues:
|
|
|
|
continue
|
|
|
|
|
|
|
|
failed_checks[name] = issues
|
|
|
|
|
|
|
|
self.assertFalse(failed_checks, pprint.pformat(failed_checks))
|
|
|
|
|
2024-10-04 02:50:16 +02:00
|
|
|
|
|
|
|
class USDExportTest(AbstractUSDTest):
|
2024-08-24 06:58:27 +02:00
|
|
|
# Utility function to round each component of a vector to a few digits. The "+ 0" is to
|
|
|
|
# ensure that any negative zeros (-0.0) are converted to positive zeros (0.0).
|
|
|
|
@staticmethod
|
|
|
|
def round_vector(vector):
|
|
|
|
return [round(c, 4) + 0 for c in vector]
|
|
|
|
|
|
|
|
# Utility function to compare two Gf.Vec3d's
|
2023-02-14 12:11:53 +01:00
|
|
|
def compareVec3d(self, first, second):
|
|
|
|
places = 5
|
|
|
|
self.assertAlmostEqual(first[0], second[0], places)
|
|
|
|
self.assertAlmostEqual(first[1], second[1], places)
|
|
|
|
self.assertAlmostEqual(first[2], second[2], places)
|
|
|
|
|
|
|
|
def test_export_extents(self):
|
|
|
|
"""Test that exported scenes contain have a properly authored extent attribute on each boundable prim"""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_extent_test.blend"))
|
|
|
|
export_path = self.tempdir / "usd_extent_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
|
|
|
|
self.export_and_validate(
|
2023-02-14 12:11:53 +01:00
|
|
|
filepath=str(export_path),
|
|
|
|
export_materials=True,
|
|
|
|
evaluation_mode="RENDER",
|
2024-05-30 20:48:43 +02:00
|
|
|
convert_world_material=False,
|
2023-02-14 12:11:53 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
# if prims are missing, the exporter must have skipped some objects
|
|
|
|
stats = UsdUtils.ComputeUsdStageStats(str(export_path))
|
2023-10-20 10:53:51 -04:00
|
|
|
self.assertEqual(stats["totalPrimCount"], 16, "Unexpected number of prims")
|
2023-02-14 12:11:53 +01:00
|
|
|
|
|
|
|
# validate the overall world bounds of the scene
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
2023-10-20 10:53:51 -04:00
|
|
|
scenePrim = stage.GetPrimAtPath("/root/scene")
|
2023-02-14 12:11:53 +01:00
|
|
|
bboxcache = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_])
|
|
|
|
bounds = bboxcache.ComputeWorldBound(scenePrim)
|
|
|
|
bound_min = bounds.GetRange().GetMin()
|
|
|
|
bound_max = bounds.GetRange().GetMax()
|
|
|
|
self.compareVec3d(bound_min, Gf.Vec3d(-5.752975881, -1, -2.798513651))
|
|
|
|
self.compareVec3d(bound_max, Gf.Vec3d(1, 2.9515805244, 2.7985136508))
|
|
|
|
|
|
|
|
# validate the locally authored extents
|
2023-10-20 10:53:51 -04:00
|
|
|
prim = stage.GetPrimAtPath("/root/scene/BigCube/BigCubeMesh")
|
2023-02-14 12:11:53 +01:00
|
|
|
extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
|
|
|
|
self.compareVec3d(Gf.Vec3d(extent[0]), Gf.Vec3d(-1, -1, -2.7985137))
|
|
|
|
self.compareVec3d(Gf.Vec3d(extent[1]), Gf.Vec3d(1, 1, 2.7985137))
|
2023-10-20 10:53:51 -04:00
|
|
|
prim = stage.GetPrimAtPath("/root/scene/LittleCube/LittleCubeMesh")
|
2023-02-14 12:11:53 +01:00
|
|
|
extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
|
|
|
|
self.compareVec3d(Gf.Vec3d(extent[0]), Gf.Vec3d(-1, -1, -1))
|
|
|
|
self.compareVec3d(Gf.Vec3d(extent[1]), Gf.Vec3d(1, 1, 1))
|
2023-10-20 10:53:51 -04:00
|
|
|
prim = stage.GetPrimAtPath("/root/scene/Volume/Volume")
|
2023-02-14 12:11:53 +01:00
|
|
|
extent = UsdGeom.Boundable(prim).GetExtentAttr().Get()
|
|
|
|
self.compareVec3d(
|
|
|
|
Gf.Vec3d(extent[0]), Gf.Vec3d(-0.7313742, -0.68043584, -0.5801515)
|
|
|
|
)
|
|
|
|
self.compareVec3d(
|
|
|
|
Gf.Vec3d(extent[1]), Gf.Vec3d(0.7515701, 0.5500924, 0.9027928)
|
|
|
|
)
|
|
|
|
|
2024-08-26 03:26:32 +02:00
|
|
|
def test_material_transforms(self):
|
|
|
|
"""Validate correct export of image mapping parameters to the UsdTransform2d shader def"""
|
|
|
|
|
|
|
|
# Use the common materials .blend file
|
2023-05-01 17:28:23 +02:00
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
|
2024-08-26 03:26:32 +02:00
|
|
|
export_path = self.tempdir / "material_transforms.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
2023-05-01 17:28:23 +02:00
|
|
|
|
2024-08-26 03:26:32 +02:00
|
|
|
# Inspect the UsdTransform2d prim on the "Transforms" material
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Transforms/Mapping")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
self.assertEqual(shader.GetIdAttr().Get(), "UsdTransform2d")
|
|
|
|
input_trans = shader.GetInput('translation')
|
|
|
|
input_rot = shader.GetInput('rotation')
|
|
|
|
input_scale = shader.GetInput('scale')
|
|
|
|
self.assertEqual(input_trans.Get(), [0.75, 0.75])
|
|
|
|
self.assertEqual(input_rot.Get(), 180)
|
|
|
|
self.assertEqual(input_scale.Get(), [0.5, 0.5])
|
|
|
|
|
|
|
|
def test_material_normal_maps(self):
|
|
|
|
"""Validate correct export of typical normal map setups to the UsdUVTexture shader def.
|
|
|
|
Namely validate that scale, bias, and ColorSpace settings are correct"""
|
|
|
|
|
|
|
|
# Use the common materials .blend file
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
|
|
|
|
export_path = self.tempdir / "material_normalmaps.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
2024-08-26 03:26:32 +02:00
|
|
|
|
|
|
|
# Inspect the UsdUVTexture prim on the "typical" "NormalMap" material
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/NormalMap/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
self.assertEqual(shader.GetIdAttr().Get(), "UsdUVTexture")
|
|
|
|
input_scale = shader.GetInput('scale')
|
|
|
|
input_bias = shader.GetInput('bias')
|
|
|
|
input_colorspace = shader.GetInput('sourceColorSpace')
|
|
|
|
self.assertEqual(input_scale.Get(), [2, 2, 2, 2])
|
|
|
|
self.assertEqual(input_bias.Get(), [-1, -1, -1, -1])
|
|
|
|
self.assertEqual(input_colorspace.Get(), 'raw')
|
|
|
|
|
|
|
|
# Inspect the UsdUVTexture prim on the "inverted" "NormalMap_Scale_Bias" material
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/NormalMap_Scale_Bias/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
self.assertEqual(shader.GetIdAttr().Get(), "UsdUVTexture")
|
|
|
|
input_scale = shader.GetInput('scale')
|
|
|
|
input_bias = shader.GetInput('bias')
|
|
|
|
input_colorspace = shader.GetInput('sourceColorSpace')
|
|
|
|
self.assertEqual(input_scale.Get(), [2, -2, 2, 1])
|
|
|
|
self.assertEqual(input_bias.Get(), [-1, 1, -1, 0])
|
|
|
|
self.assertEqual(input_colorspace.Get(), 'raw')
|
|
|
|
|
|
|
|
def test_material_opacity_threshold(self):
|
|
|
|
"""Validate correct export of opacity and opacity_threshold parameters to the UsdPreviewSurface shader def"""
|
|
|
|
|
|
|
|
# Use the common materials .blend file
|
2024-11-21 03:50:11 +01:00
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_channels.blend"))
|
|
|
|
export_path = self.tempdir / "usd_materials_channels.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
2023-05-01 17:28:23 +02:00
|
|
|
|
2024-11-21 03:50:11 +01:00
|
|
|
# Opaque no-Alpha
|
2023-05-01 17:28:23 +02:00
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
2024-11-21 03:50:11 +01:00
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Opaque/Principled_BSDF")
|
2023-05-01 17:28:23 +02:00
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
2023-05-02 08:41:10 +10:00
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), False,
|
|
|
|
"Opacity input should not be connected for opaque material")
|
2024-05-25 23:30:13 +02:00
|
|
|
self.assertAlmostEqual(opacity_input.Get(), 1.0, 2, "Opacity input should be set to 1")
|
2023-05-01 17:28:23 +02:00
|
|
|
|
2024-11-21 03:50:11 +01:00
|
|
|
# Validate Image Alpha to BSDF Alpha
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Alpha/Principled_BSDF")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
|
|
|
|
self.assertEqual(opacity_thresh_input.Get(), None, "Opacity threshold input should be empty")
|
|
|
|
|
|
|
|
# Validate Image Alpha to BSDF Alpha w/Round
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/AlphaClip_Round/Principled_BSDF")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
|
|
|
|
self.assertAlmostEqual(opacity_thresh_input.Get(), 0.5, 2, "Opacity threshold input should be 0.5")
|
|
|
|
|
|
|
|
# Validate Image Alpha to BSDF Alpha w/LessThan+Invert
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/AlphaClip_LessThan/Principled_BSDF")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
|
|
|
|
self.assertAlmostEqual(opacity_thresh_input.Get(), 0.8, 2, "Opacity threshold input should be 0.8")
|
|
|
|
|
|
|
|
# Validate Image RGB to BSDF Metallic, Roughness, Alpha
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Channel/Principled_BSDF")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
metallic_input = shader.GetInput("metallic")
|
|
|
|
roughness_input = shader.GetInput("roughness")
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
|
|
|
self.assertEqual(metallic_input.HasConnectedSource(), True, "Metallic input should be connected")
|
|
|
|
self.assertEqual(roughness_input.HasConnectedSource(), True, "Roughness input should be connected")
|
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
|
|
|
|
self.assertEqual(opacity_thresh_input.Get(), None, "Opacity threshold input should be empty")
|
|
|
|
|
|
|
|
# Validate Image RGB to BSDF Metallic, Roughness, Alpha w/Round
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/ChannelClip_Round/Principled_BSDF")
|
2023-05-01 17:28:23 +02:00
|
|
|
shader = UsdShade.Shader(shader_prim)
|
2024-11-21 03:50:11 +01:00
|
|
|
metallic_input = shader.GetInput("metallic")
|
|
|
|
roughness_input = shader.GetInput("roughness")
|
2023-05-01 17:28:23 +02:00
|
|
|
opacity_input = shader.GetInput('opacity')
|
2024-05-25 23:30:13 +02:00
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
2024-11-21 03:50:11 +01:00
|
|
|
self.assertEqual(metallic_input.HasConnectedSource(), True, "Metallic input should be connected")
|
|
|
|
self.assertEqual(roughness_input.HasConnectedSource(), True, "Roughness input should be connected")
|
2023-05-01 17:28:23 +02:00
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
|
2024-05-25 23:30:13 +02:00
|
|
|
self.assertAlmostEqual(opacity_thresh_input.Get(), 0.5, 2, "Opacity threshold input should be 0.5")
|
2023-05-01 17:28:23 +02:00
|
|
|
|
2024-11-21 03:50:11 +01:00
|
|
|
# Validate Image RGB to BSDF Metallic, Roughness, Alpha w/LessThan+Invert
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/ChannelClip_LessThan/Principled_BSDF")
|
2023-05-01 17:28:23 +02:00
|
|
|
shader = UsdShade.Shader(shader_prim)
|
2024-11-21 03:50:11 +01:00
|
|
|
metallic_input = shader.GetInput("metallic")
|
|
|
|
roughness_input = shader.GetInput("roughness")
|
2023-05-01 17:28:23 +02:00
|
|
|
opacity_input = shader.GetInput('opacity')
|
2024-05-25 23:30:13 +02:00
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
2024-11-21 03:50:11 +01:00
|
|
|
self.assertEqual(metallic_input.HasConnectedSource(), True, "Metallic input should be connected")
|
|
|
|
self.assertEqual(roughness_input.HasConnectedSource(), True, "Roughness input should be connected")
|
2023-05-01 17:28:23 +02:00
|
|
|
self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected")
|
2024-05-25 23:30:13 +02:00
|
|
|
self.assertAlmostEqual(opacity_thresh_input.Get(), 0.2, 2, "Opacity threshold input should be 0.2")
|
2023-02-14 12:11:53 +01:00
|
|
|
|
2024-09-29 23:53:11 +02:00
|
|
|
def test_export_material_subsets(self):
|
|
|
|
"""Validate multiple materials assigned to the same mesh work correctly."""
|
|
|
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_multi.blend"))
|
|
|
|
|
|
|
|
# Ensure the simulation zone data is baked for all relevant frames...
|
|
|
|
for frame in range(1, 5):
|
|
|
|
bpy.context.scene.frame_set(frame)
|
|
|
|
bpy.context.scene.frame_set(1)
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_materials_multi.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
|
2024-09-29 23:53:11 +02:00
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
# The static mesh should have 4 materials each assigned to 4 faces (16 faces total)
|
|
|
|
static_mesh_prim = UsdGeom.Mesh(stage.GetPrimAtPath("/root/static_mesh/static_mesh"))
|
|
|
|
geom_subsets = UsdGeom.Subset.GetGeomSubsets(static_mesh_prim)
|
|
|
|
self.assertEqual(len(geom_subsets), 4)
|
|
|
|
|
|
|
|
unique_face_indices = set()
|
|
|
|
for subset in geom_subsets:
|
|
|
|
face_indices = subset.GetIndicesAttr().Get()
|
|
|
|
self.assertEqual(len(face_indices), 4)
|
|
|
|
unique_face_indices.update(face_indices)
|
|
|
|
self.assertEqual(len(unique_face_indices), 16)
|
|
|
|
|
|
|
|
# The dynamic mesh varies over time (currently blocked, see #124554 and #118754)
|
|
|
|
# - Frame 1: 1 face and 1 material [mat2]
|
|
|
|
# - Frame 2: 2 faces and 2 materials [mat2, mat3]
|
|
|
|
# - Frame 3: 4 faces and 3 materials [mat2, mat3, mat2, mat1]
|
|
|
|
# - Frame 4: 4 faces and 2 materials [mat2, mat3, mat2, mat3]
|
|
|
|
dynamic_mesh_prim = UsdGeom.Mesh(stage.GetPrimAtPath("/root/dynamic_mesh/dynamic_mesh"))
|
|
|
|
geom_subsets = UsdGeom.Subset.GetGeomSubsets(dynamic_mesh_prim)
|
|
|
|
self.assertEqual(len(geom_subsets), 0)
|
|
|
|
|
2024-11-17 21:53:35 +01:00
|
|
|
def test_export_material_inmem(self):
|
|
|
|
"""Validate correct export of in memory and packed images"""
|
|
|
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_inmem_pack.blend"))
|
|
|
|
export_path1 = self.tempdir / "usd_materials_inmem_pack_relative.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path1), export_textures_mode='NEW', relative_paths=True)
|
|
|
|
|
|
|
|
export_path2 = self.tempdir / "usd_materials_inmem_pack_absolute.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path2), export_textures_mode='NEW', relative_paths=False)
|
|
|
|
|
|
|
|
# Validate that we actually see the correct set of files being saved to the filesystem
|
|
|
|
|
|
|
|
# Relative path variations
|
|
|
|
stage = Usd.Stage.Open(str(export_path1))
|
|
|
|
stage_path = pathlib.Path(stage.GetRootLayer().realPath)
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_single/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
self.assertFalse(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_udim/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
|
|
|
|
image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
|
|
|
|
self.assertFalse(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_single/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
self.assertFalse(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_udim/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
|
|
|
|
image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
|
|
|
|
self.assertFalse(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
|
|
|
|
|
|
|
|
# Absolute path variations
|
|
|
|
stage = Usd.Stage.Open(str(export_path2))
|
|
|
|
stage_path = pathlib.Path(stage.GetRootLayer().realPath)
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_single/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
self.assertTrue(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_inmem_udim/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
|
|
|
|
image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
|
|
|
|
self.assertTrue(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_single/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
self.assertTrue(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(asset_path).is_file())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/MAT_pack_udim/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
asset_path = pathlib.Path(shader.GetInput("file").GetAttr().Get().path)
|
|
|
|
image_path1 = pathlib.Path(str(asset_path).replace("<UDIM>", "1001"))
|
|
|
|
image_path2 = pathlib.Path(str(asset_path).replace("<UDIM>", "1002"))
|
|
|
|
self.assertTrue(asset_path.is_absolute())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path1).is_file())
|
|
|
|
self.assertTrue(stage_path.parent.joinpath(image_path2).is_file())
|
|
|
|
|
2025-01-10 06:34:27 +01:00
|
|
|
def test_export_material_textures_mode(self):
|
|
|
|
"""Validate the non-default export textures mode options."""
|
|
|
|
|
|
|
|
# Use the common materials .blend file
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
|
|
|
|
|
|
|
|
# For this test, the "textures" directory should NOT exist and the image paths
|
|
|
|
# should all point to the original test location, not the temp output location.
|
|
|
|
def check_image_paths(stage):
|
|
|
|
orig_tex_path = (self.testdir / "textures")
|
|
|
|
temp_tex_path = (self.tempdir / "textures")
|
|
|
|
self.assertFalse(temp_tex_path.is_dir())
|
|
|
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Material/Image_Texture")
|
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
filepath = pathlib.Path(shader.GetInput('file').Get().path)
|
|
|
|
self.assertEqual(orig_tex_path, filepath.parent)
|
|
|
|
|
|
|
|
export_file = str(self.tempdir / "usd_materials_texture_preserve.usda")
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=export_file, export_materials=True, convert_world_material=False, export_textures_mode='PRESERVE')
|
|
|
|
check_image_paths(Usd.Stage.Open(export_file))
|
|
|
|
|
|
|
|
export_file = str(self.tempdir / "usd_materials_texture_keep.usda")
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=export_file, export_materials=True, convert_world_material=False, export_textures_mode='KEEP')
|
|
|
|
check_image_paths(Usd.Stage.Open(export_file))
|
|
|
|
|
2024-11-05 20:37:36 +01:00
|
|
|
def test_export_material_displacement(self):
|
|
|
|
"""Validate correct export of Displacement information for the UsdPreviewSurface"""
|
|
|
|
|
|
|
|
# Use the common materials .blend file
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_displace.blend"))
|
|
|
|
export_path = self.tempdir / "material_displace.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
# Verify "constant" displacement
|
|
|
|
shader_surface = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/constant/Principled_BSDF"))
|
|
|
|
self.assertEqual(shader_surface.GetIdAttr().Get(), "UsdPreviewSurface")
|
|
|
|
input_displacement = shader_surface.GetInput('displacement')
|
|
|
|
self.assertEqual(input_displacement.HasConnectedSource(), False, "Displacement input should not be connected")
|
|
|
|
self.assertAlmostEqual(input_displacement.Get(), 0.45, 5)
|
|
|
|
|
|
|
|
# Validate various Midlevel and Scale scenarios
|
|
|
|
def validate_displacement(mat_name, expected_scale, expected_bias):
|
|
|
|
shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/{mat_name}/Principled_BSDF"))
|
|
|
|
shader_image = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/{mat_name}/Image_Texture"))
|
|
|
|
self.assertEqual(shader_surface.GetIdAttr().Get(), "UsdPreviewSurface")
|
|
|
|
self.assertEqual(shader_image.GetIdAttr().Get(), "UsdUVTexture")
|
|
|
|
input_displacement = shader_surface.GetInput('displacement')
|
|
|
|
input_colorspace = shader_image.GetInput('sourceColorSpace')
|
|
|
|
input_scale = shader_image.GetInput('scale')
|
|
|
|
input_bias = shader_image.GetInput('bias')
|
|
|
|
self.assertEqual(input_displacement.HasConnectedSource(), True, "Displacement input should be connected")
|
|
|
|
self.assertEqual(input_colorspace.Get(), 'raw')
|
|
|
|
self.assertEqual(self.round_vector(input_scale.Get()), expected_scale)
|
|
|
|
self.assertEqual(self.round_vector(input_bias.Get()), expected_bias)
|
|
|
|
|
|
|
|
validate_displacement("mid_0_0", [1.0, 1.0, 1.0, 1.0], [0, 0, 0, 0])
|
|
|
|
validate_displacement("mid_0_5", [1.0, 1.0, 1.0, 1.0], [-0.5, -0.5, -0.5, 0])
|
|
|
|
validate_displacement("mid_1_0", [1.0, 1.0, 1.0, 1.0], [-1, -1, -1, 0])
|
|
|
|
validate_displacement("mid_0_0_scale_0_3", [0.3, 0.3, 0.3, 1.0], [0, 0, 0, 0])
|
|
|
|
validate_displacement("mid_0_5_scale_0_3", [0.3, 0.3, 0.3, 1.0], [-0.15, -0.15, -0.15, 0])
|
|
|
|
validate_displacement("mid_1_0_scale_0_3", [0.3, 0.3, 0.3, 1.0], [-0.3, -0.3, -0.3, 0])
|
|
|
|
|
|
|
|
# Validate that no displacement occurs for scenarios USD doesn't support
|
|
|
|
shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/bad_wrong_space/Principled_BSDF"))
|
|
|
|
input_displacement = shader_surface.GetInput('displacement')
|
|
|
|
self.assertTrue(input_displacement.Get() is None)
|
|
|
|
shader_surface = UsdShade.Shader(stage.GetPrimAtPath(f"/root/_materials/bad_non_const/Principled_BSDF"))
|
|
|
|
input_displacement = shader_surface.GetInput('displacement')
|
|
|
|
self.assertTrue(input_displacement.Get() is None)
|
|
|
|
|
2025-05-19 19:47:22 +02:00
|
|
|
def test_export_material_attributes(self):
|
|
|
|
"""Validate correct export of Attribute information to UsdPrimvarReaders"""
|
|
|
|
|
|
|
|
# Use the common materials .blend file
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_attributes.blend"))
|
|
|
|
export_path = self.tempdir / "usd_materials_attributes.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path), export_materials=True)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
shader_attr = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/Material/Attribute"))
|
|
|
|
shader_attr1 = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/Material/Attribute_001"))
|
|
|
|
shader_attr2 = UsdShade.Shader(stage.GetPrimAtPath("/root/_materials/Material/Attribute_002"))
|
|
|
|
|
|
|
|
self.assertEqual(shader_attr.GetIdAttr().Get(), "UsdPrimvarReader_float3")
|
|
|
|
self.assertEqual(shader_attr1.GetIdAttr().Get(), "UsdPrimvarReader_float")
|
|
|
|
self.assertEqual(shader_attr2.GetIdAttr().Get(), "UsdPrimvarReader_vector")
|
|
|
|
|
|
|
|
self.assertEqual(shader_attr.GetInput("varname").Get(), "displayColor")
|
|
|
|
self.assertEqual(shader_attr1.GetInput("varname").Get(), "f_float")
|
|
|
|
self.assertEqual(shader_attr2.GetInput("varname").Get(), "f_vec")
|
|
|
|
|
|
|
|
self.assertEqual(shader_attr.GetOutput("result").GetTypeName().type.typeName, "GfVec3f")
|
|
|
|
self.assertEqual(shader_attr1.GetOutput("result").GetTypeName().type.typeName, "float")
|
|
|
|
self.assertEqual(shader_attr2.GetOutput("result").GetTypeName().type.typeName, "GfVec3f")
|
|
|
|
|
2024-12-16 22:11:07 +01:00
|
|
|
def test_export_metaballs(self):
|
|
|
|
"""Validate correct export of Metaball objects. These are written out as Meshes."""
|
|
|
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_metaballs.blend"))
|
|
|
|
export_path = self.tempdir / "usd_metaballs.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER")
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
# There should be 3 Mesh prims and they should each correspond to the "basis"
|
|
|
|
# metaball (i.e. the ones without any numeric suffix)
|
|
|
|
mesh_prims = [prim for prim in stage.Traverse() if prim.IsA(UsdGeom.Mesh)]
|
|
|
|
prim_names = [prim.GetPath().pathString for prim in mesh_prims]
|
|
|
|
self.assertEqual(len(mesh_prims), 3)
|
|
|
|
self.assertListEqual(
|
|
|
|
sorted(prim_names), ["/root/Ball_A/Ball_A", "/root/Ball_B/Ball_B", "/root/Ball_C/Ball_C"])
|
|
|
|
|
|
|
|
# Make rough check of vertex counts to ensure geometry is present
|
|
|
|
actual_prim_verts = {prim.GetName(): len(UsdGeom.Mesh(prim).GetPointsAttr().Get()) for prim in mesh_prims}
|
|
|
|
expected_prim_verts = {"Ball_A": 2232, "Ball_B": 2876, "Ball_C": 1152}
|
|
|
|
self.assertDictEqual(actual_prim_verts, expected_prim_verts)
|
|
|
|
|
2024-12-17 01:19:51 +01:00
|
|
|
def test_particle_hair(self):
|
|
|
|
"""Validate correct export of particle hair emitters."""
|
|
|
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_particle_hair.blend"))
|
|
|
|
|
|
|
|
# Ensure the hair dynamics are baked for all relevant frames...
|
|
|
|
for frame in range(1, 11):
|
|
|
|
bpy.context.scene.frame_set(frame)
|
|
|
|
bpy.context.scene.frame_set(1)
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_particle_hair.usda"
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=str(export_path), export_hair=True, export_animation=True, evaluation_mode="RENDER")
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
main_prim = stage.GetPrimAtPath("/root/Sphere")
|
|
|
|
hair_prim = stage.GetPrimAtPath("/root/Sphere/ParticleSystem")
|
|
|
|
self.assertTrue(main_prim.IsValid())
|
|
|
|
self.assertTrue(hair_prim.IsValid())
|
|
|
|
|
|
|
|
# Ensure we have 5 frames of rotation data for the main Sphere and 10 frames for the hair data
|
|
|
|
rot_samples = UsdGeom.Xformable(main_prim).GetRotateXYZOp().GetTimeSamples()
|
|
|
|
self.assertEqual(len(rot_samples), 5)
|
|
|
|
|
|
|
|
hair_curves = UsdGeom.BasisCurves(hair_prim)
|
|
|
|
hair_samples = hair_curves.GetPointsAttr().GetTimeSamples()
|
|
|
|
self.assertEqual(hair_curves.GetTypeAttr().Get(), "cubic")
|
|
|
|
self.assertEqual(hair_curves.GetBasisAttr().Get(), "bspline")
|
|
|
|
self.assertEqual(len(hair_samples), 10)
|
|
|
|
|
2024-04-28 03:53:20 +02:00
|
|
|
def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len):
|
|
|
|
pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)
|
|
|
|
self.assertTrue(pv.HasValue())
|
|
|
|
self.assertEqual(pv.GetTypeName().type.typeName, pv_typeName)
|
|
|
|
self.assertEqual(pv.GetInterpolation(), pv_interp)
|
|
|
|
self.assertEqual(len(pv.Get()), elements_len)
|
|
|
|
|
|
|
|
def check_primvar_missing(self, prim, pv_name):
|
|
|
|
pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)
|
|
|
|
self.assertFalse(pv.HasValue())
|
|
|
|
|
|
|
|
def test_export_attributes(self):
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_test.blend"))
|
|
|
|
export_path = self.tempdir / "usd_attribute_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER")
|
2024-04-28 03:53:20 +02:00
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
# Validate all expected Mesh attributes. Notice that nothing on
|
|
|
|
# the Edge domain is supported by USD.
|
|
|
|
prim = stage.GetPrimAtPath("/root/Mesh/Mesh")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "p_bool", "VtArray<bool>", "vertex", 4)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "p_int8", "VtArray<unsigned char>", "vertex", 4)
|
2024-04-28 03:53:20 +02:00
|
|
|
self.check_primvar(prim, "p_int32", "VtArray<int>", "vertex", 4)
|
|
|
|
self.check_primvar(prim, "p_float", "VtArray<float>", "vertex", 4)
|
2024-08-04 04:34:15 +02:00
|
|
|
self.check_primvar(prim, "p_color", "VtArray<GfVec4f>", "vertex", 4)
|
|
|
|
self.check_primvar(prim, "p_byte_color", "VtArray<GfVec4f>", "vertex", 4)
|
2024-04-28 03:53:20 +02:00
|
|
|
self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "vertex", 4)
|
|
|
|
self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "vertex", 4)
|
|
|
|
self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "vertex", 4)
|
|
|
|
self.check_primvar_missing(prim, "p_mat4x4")
|
|
|
|
|
|
|
|
self.check_primvar_missing(prim, "e_bool")
|
|
|
|
self.check_primvar_missing(prim, "e_int8")
|
|
|
|
self.check_primvar_missing(prim, "e_int32")
|
|
|
|
self.check_primvar_missing(prim, "e_float")
|
|
|
|
self.check_primvar_missing(prim, "e_color")
|
|
|
|
self.check_primvar_missing(prim, "e_byte_color")
|
|
|
|
self.check_primvar_missing(prim, "e_vec2")
|
|
|
|
self.check_primvar_missing(prim, "e_vec3")
|
|
|
|
self.check_primvar_missing(prim, "e_quat")
|
|
|
|
self.check_primvar_missing(prim, "e_mat4x4")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "f_bool", "VtArray<bool>", "uniform", 1)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "f_int8", "VtArray<unsigned char>", "uniform", 1)
|
2024-04-28 03:53:20 +02:00
|
|
|
self.check_primvar(prim, "f_int32", "VtArray<int>", "uniform", 1)
|
|
|
|
self.check_primvar(prim, "f_float", "VtArray<float>", "uniform", 1)
|
2024-09-24 19:05:55 +02:00
|
|
|
self.check_primvar(prim, "f_color", "VtArray<GfVec4f>", "uniform", 1)
|
|
|
|
self.check_primvar(prim, "f_byte_color", "VtArray<GfVec4f>", "uniform", 1)
|
2024-10-19 20:59:25 +02:00
|
|
|
self.check_primvar(prim, "displayColor", "VtArray<GfVec3f>", "uniform", 1)
|
2024-04-28 03:53:20 +02:00
|
|
|
self.check_primvar(prim, "f_vec2", "VtArray<GfVec2f>", "uniform", 1)
|
|
|
|
self.check_primvar(prim, "f_vec3", "VtArray<GfVec3f>", "uniform", 1)
|
|
|
|
self.check_primvar(prim, "f_quat", "VtArray<GfQuatf>", "uniform", 1)
|
|
|
|
self.check_primvar_missing(prim, "f_mat4x4")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "fc_bool", "VtArray<bool>", "faceVarying", 4)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "fc_int8", "VtArray<unsigned char>", "faceVarying", 4)
|
2024-04-28 03:53:20 +02:00
|
|
|
self.check_primvar(prim, "fc_int32", "VtArray<int>", "faceVarying", 4)
|
|
|
|
self.check_primvar(prim, "fc_float", "VtArray<float>", "faceVarying", 4)
|
2024-08-04 04:34:15 +02:00
|
|
|
self.check_primvar(prim, "fc_color", "VtArray<GfVec4f>", "faceVarying", 4)
|
|
|
|
self.check_primvar(prim, "fc_byte_color", "VtArray<GfVec4f>", "faceVarying", 4)
|
2024-04-28 03:53:20 +02:00
|
|
|
self.check_primvar(prim, "fc_vec2", "VtArray<GfVec2f>", "faceVarying", 4)
|
|
|
|
self.check_primvar(prim, "fc_vec3", "VtArray<GfVec3f>", "faceVarying", 4)
|
|
|
|
self.check_primvar(prim, "fc_quat", "VtArray<GfQuatf>", "faceVarying", 4)
|
|
|
|
self.check_primvar_missing(prim, "fc_mat4x4")
|
|
|
|
|
2024-05-25 22:23:40 +02:00
|
|
|
prim = stage.GetPrimAtPath("/root/Curve_base/Curves/Curves")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "p_bool", "VtArray<bool>", "vertex", 24)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "p_int8", "VtArray<unsigned char>", "vertex", 24)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "p_int32", "VtArray<int>", "vertex", 24)
|
|
|
|
self.check_primvar(prim, "p_float", "VtArray<float>", "vertex", 24)
|
2024-09-24 19:05:55 +02:00
|
|
|
self.check_primvar(prim, "p_color", "VtArray<GfVec4f>", "vertex", 24)
|
|
|
|
self.check_primvar(prim, "p_byte_color", "VtArray<GfVec4f>", "vertex", 24)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "vertex", 24)
|
|
|
|
self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "vertex", 24)
|
|
|
|
self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "vertex", 24)
|
|
|
|
self.check_primvar_missing(prim, "p_mat4x4")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "sp_bool", "VtArray<bool>", "uniform", 2)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "sp_int8", "VtArray<unsigned char>", "uniform", 2)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "sp_int32", "VtArray<int>", "uniform", 2)
|
|
|
|
self.check_primvar(prim, "sp_float", "VtArray<float>", "uniform", 2)
|
2024-09-24 19:05:55 +02:00
|
|
|
self.check_primvar(prim, "sp_color", "VtArray<GfVec4f>", "uniform", 2)
|
|
|
|
self.check_primvar(prim, "sp_byte_color", "VtArray<GfVec4f>", "uniform", 2)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "sp_vec2", "VtArray<GfVec2f>", "uniform", 2)
|
|
|
|
self.check_primvar(prim, "sp_vec3", "VtArray<GfVec3f>", "uniform", 2)
|
|
|
|
self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "uniform", 2)
|
|
|
|
self.check_primvar_missing(prim, "sp_mat4x4")
|
|
|
|
|
|
|
|
prim = stage.GetPrimAtPath("/root/Curve_bezier_base/Curves_bezier/Curves")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "p_bool", "VtArray<bool>", "varying", 10)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "p_int8", "VtArray<unsigned char>", "varying", 10)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "p_int32", "VtArray<int>", "varying", 10)
|
|
|
|
self.check_primvar(prim, "p_float", "VtArray<float>", "varying", 10)
|
2024-09-24 19:05:55 +02:00
|
|
|
self.check_primvar(prim, "p_color", "VtArray<GfVec4f>", "varying", 10)
|
|
|
|
self.check_primvar(prim, "p_byte_color", "VtArray<GfVec4f>", "varying", 10)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "p_vec2", "VtArray<GfVec2f>", "varying", 10)
|
|
|
|
self.check_primvar(prim, "p_vec3", "VtArray<GfVec3f>", "varying", 10)
|
|
|
|
self.check_primvar(prim, "p_quat", "VtArray<GfQuatf>", "varying", 10)
|
|
|
|
self.check_primvar_missing(prim, "p_mat4x4")
|
|
|
|
|
|
|
|
self.check_primvar(prim, "sp_bool", "VtArray<bool>", "uniform", 3)
|
2024-06-02 22:26:12 +02:00
|
|
|
self.check_primvar(prim, "sp_int8", "VtArray<unsigned char>", "uniform", 3)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "sp_int32", "VtArray<int>", "uniform", 3)
|
|
|
|
self.check_primvar(prim, "sp_float", "VtArray<float>", "uniform", 3)
|
2024-09-24 19:05:55 +02:00
|
|
|
self.check_primvar(prim, "sp_color", "VtArray<GfVec4f>", "uniform", 3)
|
|
|
|
self.check_primvar(prim, "sp_byte_color", "VtArray<GfVec4f>", "uniform", 3)
|
2024-05-25 22:23:40 +02:00
|
|
|
self.check_primvar(prim, "sp_vec2", "VtArray<GfVec2f>", "uniform", 3)
|
|
|
|
self.check_primvar(prim, "sp_vec3", "VtArray<GfVec3f>", "uniform", 3)
|
|
|
|
self.check_primvar(prim, "sp_quat", "VtArray<GfQuatf>", "uniform", 3)
|
|
|
|
self.check_primvar_missing(prim, "sp_mat4x4")
|
|
|
|
|
2024-08-11 23:36:40 +02:00
|
|
|
def test_export_attributes_varying(self):
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_attribute_varying_test.blend"))
|
|
|
|
# Ensure the simulation zone data is baked for all relevant frames...
|
|
|
|
for frame in range(1, 16):
|
|
|
|
bpy.context.scene.frame_set(frame)
|
|
|
|
bpy.context.scene.frame_set(1)
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_attribute_varying_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
|
2024-08-11 23:36:40 +02:00
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
2024-09-20 19:57:12 +02:00
|
|
|
sparse_frames = [4.0, 5.0, 8.0, 9.0, 12.0, 13.0]
|
2024-08-11 23:36:40 +02:00
|
|
|
|
|
|
|
#
|
|
|
|
# Validate Mesh data
|
|
|
|
#
|
|
|
|
mesh1 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh1/mesh1"))
|
|
|
|
mesh2 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh2/mesh2"))
|
|
|
|
mesh3 = UsdGeom.Mesh(stage.GetPrimAtPath("/root/mesh3/mesh3"))
|
|
|
|
|
|
|
|
# Positions (should be sparsely written)
|
|
|
|
self.assertEqual(mesh1.GetPointsAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(mesh2.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(mesh3.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
# Velocity (should be sparsely written)
|
|
|
|
self.assertEqual(mesh1.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(mesh2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(mesh3.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
# Regular primvar (should be sparsely written)
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(mesh1).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(mesh2).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(mesh3).GetPrimvar("test").GetTimeSamples(), sparse_frames)
|
2025-01-17 03:28:13 +01:00
|
|
|
# Extents of the mesh (should be sparsely written)
|
|
|
|
self.assertEqual(UsdGeom.Boundable(mesh1).GetExtentAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(UsdGeom.Boundable(mesh2).GetExtentAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.Boundable(mesh3).GetExtentAttr().GetTimeSamples(), [])
|
2024-08-11 23:36:40 +02:00
|
|
|
|
2024-09-20 19:57:12 +02:00
|
|
|
#
|
|
|
|
# Validate PointCloud data
|
|
|
|
#
|
|
|
|
points1 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud1/PointCloud"))
|
|
|
|
points2 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud2/PointCloud"))
|
|
|
|
points3 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud3/PointCloud"))
|
|
|
|
points4 = UsdGeom.Points(stage.GetPrimAtPath("/root/pointcloud4/PointCloud"))
|
|
|
|
|
|
|
|
# Positions (should be sparsely written)
|
|
|
|
self.assertEqual(points1.GetPointsAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(points2.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(points3.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(points4.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
# Velocity (should be sparsely written)
|
|
|
|
self.assertEqual(points1.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(points2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(points3.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(points4.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
# Radius (should be sparsely written)
|
|
|
|
self.assertEqual(points1.GetWidthsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(points2.GetWidthsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(points3.GetWidthsAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(points4.GetWidthsAttr().GetTimeSamples(), [])
|
|
|
|
# Regular primvar (should be sparsely written)
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(points1).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(points2).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(points3).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(points4).GetPrimvar("test").GetTimeSamples(), sparse_frames)
|
|
|
|
# Extents of the point cloud (should be sparsely written)
|
|
|
|
self.assertEqual(UsdGeom.Boundable(points1).GetExtentAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(UsdGeom.Boundable(points2).GetExtentAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.Boundable(points3).GetExtentAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(UsdGeom.Boundable(points4).GetExtentAttr().GetTimeSamples(), [])
|
|
|
|
|
2025-01-15 23:29:42 +01:00
|
|
|
#
|
|
|
|
# Validate BasisCurve data
|
|
|
|
#
|
|
|
|
curves1 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane1/curves1/Curves"))
|
|
|
|
curves2 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane2/curves2/Curves"))
|
|
|
|
curves3 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane3/curves3/Curves"))
|
|
|
|
curves4 = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/curves_plane4/curves4/Curves"))
|
|
|
|
|
|
|
|
# Positions (should be sparsely written)
|
|
|
|
self.assertEqual(curves1.GetPointsAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(curves2.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(curves3.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(curves4.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
# Velocity (should be sparsely written)
|
|
|
|
self.assertEqual(curves1.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(curves2.GetVelocitiesAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(curves3.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(curves4.GetVelocitiesAttr().GetTimeSamples(), [])
|
|
|
|
# Radius (should be sparsely written)
|
|
|
|
self.assertEqual(curves1.GetWidthsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(curves2.GetWidthsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(curves3.GetWidthsAttr().GetTimeSamples(), sparse_frames)
|
|
|
|
self.assertEqual(curves4.GetWidthsAttr().GetTimeSamples(), [])
|
|
|
|
# Regular primvar (should be sparsely written)
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(curves1).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(curves2).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(curves3).GetPrimvar("test").GetTimeSamples(), [])
|
|
|
|
self.assertEqual(UsdGeom.PrimvarsAPI(curves4).GetPrimvar("test").GetTimeSamples(), sparse_frames)
|
|
|
|
|
2024-08-12 01:50:34 +02:00
|
|
|
def test_export_mesh_subd(self):
|
|
|
|
"""Test exporting Subdivision Surface attributes and values"""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_mesh_subd.blend"))
|
|
|
|
export_path = self.tempdir / "usd_mesh_subd.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(
|
2024-08-12 01:50:34 +02:00
|
|
|
filepath=str(export_path),
|
|
|
|
export_subdivision='BEST_MATCH',
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_none_boundary_smooth_all/mesh1"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'all')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_corners_boundary_smooth_all/mesh2"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'cornersOnly')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_corners_junctions_boundary_smooth_all/mesh3"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'cornersPlus1')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_corners_junctions_concave_boundary_smooth_all/mesh4"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'cornersPlus2')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_boundaries_boundary_smooth_all/mesh5"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_all_boundary_smooth_all/mesh6"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'none')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/uv_smooth_boundaries_boundary_smooth_keep/mesh7"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeAndCorner')
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/crease_verts/crease_verts"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
self.assertEqual(len(mesh.GetCornerIndicesAttr().Get()), 7)
|
|
|
|
usd_vert_sharpness = mesh.GetCornerSharpnessesAttr().Get()
|
|
|
|
self.assertEqual(len(usd_vert_sharpness), 7)
|
|
|
|
# A 1.0 crease is INFINITE (10) in USD
|
|
|
|
self.assertAlmostEqual(min(usd_vert_sharpness), 0.1, 5)
|
2025-02-03 04:38:58 +01:00
|
|
|
self.assertEqual(len([sharp for sharp in usd_vert_sharpness if sharp < 10]), 6)
|
2024-08-12 01:50:34 +02:00
|
|
|
self.assertEqual(len([sharp for sharp in usd_vert_sharpness if sharp == 10]), 1)
|
|
|
|
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/crease_edge/crease_edge"))
|
|
|
|
self.assertEqual(mesh.GetSubdivisionSchemeAttr().Get(), 'catmullClark')
|
|
|
|
self.assertEqual(mesh.GetFaceVaryingLinearInterpolationAttr().Get(), 'boundaries')
|
|
|
|
self.assertEqual(mesh.GetInterpolateBoundaryAttr().Get(), 'edgeOnly')
|
|
|
|
self.assertEqual(len(mesh.GetCreaseIndicesAttr().Get()), 20)
|
|
|
|
usd_crease_lengths = mesh.GetCreaseLengthsAttr().Get()
|
|
|
|
self.assertEqual(len(usd_crease_lengths), 10)
|
|
|
|
self.assertTrue(all([length == 2 for length in usd_crease_lengths]))
|
|
|
|
usd_crease_sharpness = mesh.GetCreaseSharpnessesAttr().Get()
|
|
|
|
self.assertEqual(len(usd_crease_sharpness), 10)
|
|
|
|
# A 1.0 crease is INFINITE (10) in USD
|
|
|
|
self.assertAlmostEqual(min(usd_crease_sharpness), 0.1, 5)
|
2025-02-03 04:38:58 +01:00
|
|
|
self.assertEqual(len([sharp for sharp in usd_crease_sharpness if sharp < 10]), 9)
|
2024-08-12 01:50:34 +02:00
|
|
|
self.assertEqual(len([sharp for sharp in usd_crease_sharpness if sharp == 10]), 1)
|
|
|
|
|
2024-08-13 08:07:15 +02:00
|
|
|
def test_export_mesh_triangulate(self):
|
|
|
|
"""Test exporting with different triangulation options for meshes."""
|
|
|
|
|
|
|
|
# Use the current scene to create simple geometry to triangulate
|
|
|
|
bpy.ops.mesh.primitive_plane_add(size=1)
|
|
|
|
bpy.ops.mesh.primitive_circle_add(fill_type='NGON', radius=1, vertices=7)
|
|
|
|
|
|
|
|
# We assume that triangulation is thoroughly tested elsewhere. Here we are only interested
|
|
|
|
# in checking that USD passes its operator properties through correctly. We use a minimal
|
|
|
|
# combination of quad and ngon methods to test.
|
|
|
|
tri_export_path1 = self.tempdir / "usd_mesh_tri_setup1.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(
|
2024-08-13 08:07:15 +02:00
|
|
|
filepath=str(tri_export_path1),
|
|
|
|
triangulate_meshes=True,
|
|
|
|
quad_method='FIXED',
|
|
|
|
ngon_method='BEAUTY',
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
)
|
|
|
|
|
|
|
|
tri_export_path2 = self.tempdir / "usd_mesh_tri_setup2.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(
|
2024-08-13 08:07:15 +02:00
|
|
|
filepath=str(tri_export_path2),
|
|
|
|
triangulate_meshes=True,
|
|
|
|
quad_method='FIXED_ALTERNATE',
|
|
|
|
ngon_method='CLIP',
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
)
|
|
|
|
|
|
|
|
stage1 = Usd.Stage.Open(str(tri_export_path1))
|
|
|
|
stage2 = Usd.Stage.Open(str(tri_export_path2))
|
|
|
|
|
|
|
|
# The Plane should have different vertex ordering because of the quad methods chosen
|
|
|
|
plane1 = UsdGeom.Mesh(stage1.GetPrimAtPath("/root/Plane/Plane"))
|
|
|
|
plane2 = UsdGeom.Mesh(stage2.GetPrimAtPath("/root/Plane/Plane"))
|
|
|
|
indices1 = plane1.GetFaceVertexIndicesAttr().Get()
|
|
|
|
indices2 = plane2.GetFaceVertexIndicesAttr().Get()
|
|
|
|
self.assertEqual(len(indices1), 6)
|
|
|
|
self.assertEqual(len(indices2), 6)
|
|
|
|
self.assertNotEqual(indices1, indices2)
|
|
|
|
|
|
|
|
# The Circle should have different vertex ordering because of the ngon methods chosen
|
|
|
|
circle1 = UsdGeom.Mesh(stage1.GetPrimAtPath("/root/Circle/Circle"))
|
|
|
|
circle2 = UsdGeom.Mesh(stage2.GetPrimAtPath("/root/Circle/Circle"))
|
|
|
|
indices1 = circle1.GetFaceVertexIndicesAttr().Get()
|
|
|
|
indices2 = circle2.GetFaceVertexIndicesAttr().Get()
|
|
|
|
self.assertEqual(len(indices1), 15)
|
|
|
|
self.assertEqual(len(indices2), 15)
|
|
|
|
self.assertNotEqual(indices1, indices2)
|
|
|
|
|
2025-01-17 03:28:13 +01:00
|
|
|
def test_export_curves(self):
|
|
|
|
"""Test exporting Curve types"""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_curves_test.blend"))
|
|
|
|
export_path = self.tempdir / "usd_curves_test.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path), evaluation_mode="RENDER")
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
def check_basis_curve(prim, basis, curve_type, wrap, vert_counts, extent):
|
|
|
|
self.assertEqual(prim.GetBasisAttr().Get(), basis)
|
|
|
|
self.assertEqual(prim.GetTypeAttr().Get(), curve_type)
|
|
|
|
self.assertEqual(prim.GetWrapAttr().Get(), wrap)
|
|
|
|
self.assertEqual(prim.GetWidthsInterpolation(), "varying" if basis == "bezier" else "vertex")
|
|
|
|
self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts)
|
|
|
|
usd_extent = prim.GetExtentAttr().Get()
|
|
|
|
self.assertEqual(self.round_vector(usd_extent[0]), extent[0])
|
|
|
|
self.assertEqual(self.round_vector(usd_extent[1]), extent[1])
|
|
|
|
|
|
|
|
def check_nurbs_curve(prim, cyclic, orders, vert_counts, knots_count, extent):
|
|
|
|
self.assertEqual(prim.GetOrderAttr().Get(), orders)
|
|
|
|
self.assertEqual(prim.GetCurveVertexCountsAttr().Get(), vert_counts)
|
|
|
|
self.assertEqual(prim.GetWidthsInterpolation(), "vertex")
|
|
|
|
knots = prim.GetKnotsAttr().Get()
|
|
|
|
usd_extent = prim.GetExtentAttr().Get()
|
|
|
|
self.assertEqual(self.round_vector(usd_extent[0]), extent[0])
|
|
|
|
self.assertEqual(self.round_vector(usd_extent[1]), extent[1])
|
|
|
|
|
|
|
|
curve_count = len(vert_counts)
|
|
|
|
self.assertEqual(len(knots), knots_count * curve_count)
|
|
|
|
if not cyclic:
|
|
|
|
for i in range(0, curve_count):
|
|
|
|
zeroth_knot = i * len(knots) // curve_count
|
|
|
|
self.assertEqual(knots[zeroth_knot], knots[zeroth_knot + 1], "Knots start rule violated")
|
|
|
|
self.assertEqual(
|
|
|
|
knots[zeroth_knot + knots_count - 1],
|
|
|
|
knots[zeroth_knot + knots_count - 2],
|
|
|
|
"Knots end rule violated")
|
|
|
|
else:
|
|
|
|
self.assertEqual(curve_count, 1, "Validation is only correct for 1 cyclic curve currently")
|
|
|
|
self.assertEqual(
|
|
|
|
knots[0], knots[1] - (knots[knots_count - 2] - knots[knots_count - 3]), "Knots rule violated")
|
|
|
|
self.assertEqual(
|
|
|
|
knots[knots_count - 1], knots[knots_count - 2] + (knots[2] - knots[1]), "Knots rule violated")
|
|
|
|
|
|
|
|
# Contains 3 CatmullRom curves
|
|
|
|
curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/Cube/Curves/Curves"))
|
|
|
|
check_basis_curve(
|
2025-03-07 17:38:29 +01:00
|
|
|
curve, "catmullRom", "cubic", "pinned", [8, 8, 8], [[-0.3884, -0.0966, 0.99], [0.2814, -0.0388, 1.31]])
|
2025-01-17 03:28:13 +01:00
|
|
|
|
|
|
|
# Contains 1 Bezier curve
|
|
|
|
curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCurve/BezierCurve"))
|
2025-03-07 17:38:29 +01:00
|
|
|
check_basis_curve(curve, "bezier", "cubic", "nonperiodic", [7], [[-3.644, -1.0777, -1.0], [2.0, 1.9815, 1.0]])
|
2025-01-17 03:28:13 +01:00
|
|
|
|
|
|
|
# Contains 1 Bezier curve
|
|
|
|
curve = UsdGeom.BasisCurves(stage.GetPrimAtPath("/root/BezierCircle/BezierCircle"))
|
2025-03-07 17:38:29 +01:00
|
|
|
check_basis_curve(curve, "bezier", "cubic", "periodic", [12], [[-2.0, -2.0, -1.0], [2.0, 2.0, 1.0]])
|
2025-01-17 03:28:13 +01:00
|
|
|
|
|
|
|
# Contains 2 NURBS curves
|
|
|
|
curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCurve/NurbsCurve"))
|
2025-06-12 16:22:21 +02:00
|
|
|
check_nurbs_curve(curve, False, [4, 4], [6, 6], 10, [[-1.75, -2.6891, -1.0117], [3.0896, 1.9583, 1.0293]])
|
2025-01-17 03:28:13 +01:00
|
|
|
|
|
|
|
# Contains 1 NURBS curve
|
|
|
|
curve = UsdGeom.NurbsCurves(stage.GetPrimAtPath("/root/NurbsCircle/NurbsCircle"))
|
2025-03-07 17:38:29 +01:00
|
|
|
check_nurbs_curve(curve, True, [3], [8], 13, [[-2.0, -2.0, -1.0], [2.0, 2.0, 1.0]])
|
2025-01-17 03:28:13 +01:00
|
|
|
|
2024-08-24 06:58:27 +02:00
|
|
|
def test_export_animation(self):
|
2024-06-20 03:50:26 +02:00
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend"))
|
|
|
|
export_path = self.tempdir / "usd_anim_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(
|
2024-06-20 03:50:26 +02:00
|
|
|
filepath=str(export_path),
|
|
|
|
export_animation=True,
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
# Validate the simple object animation
|
|
|
|
prim = stage.GetPrimAtPath("/root/cube_anim_xform")
|
|
|
|
self.assertEqual(prim.GetTypeName(), "Xform")
|
|
|
|
loc_samples = UsdGeom.Xformable(prim).GetTranslateOp().GetTimeSamples()
|
|
|
|
rot_samples = UsdGeom.Xformable(prim).GetRotateXYZOp().GetTimeSamples()
|
|
|
|
scale_samples = UsdGeom.Xformable(prim).GetScaleOp().GetTimeSamples()
|
|
|
|
self.assertEqual(loc_samples, [1.0, 2.0, 3.0, 4.0])
|
|
|
|
self.assertEqual(rot_samples, [1.0])
|
|
|
|
self.assertEqual(scale_samples, [1.0])
|
|
|
|
|
2025-01-26 04:43:16 +01:00
|
|
|
prim = stage.GetPrimAtPath("/root/cube_anim_xform/cube_anim_child")
|
|
|
|
self.assertEqual(prim.GetTypeName(), "Xform")
|
|
|
|
loc_samples = UsdGeom.Xformable(prim).GetTranslateOp().GetTimeSamples()
|
|
|
|
rot_samples = UsdGeom.Xformable(prim).GetRotateXYZOp().GetTimeSamples()
|
|
|
|
scale_samples = UsdGeom.Xformable(prim).GetScaleOp().GetTimeSamples()
|
|
|
|
self.assertEqual(loc_samples, [1.0])
|
|
|
|
self.assertEqual(rot_samples, [1.0, 2.0, 3.0, 4.0])
|
|
|
|
self.assertEqual(scale_samples, [1.0])
|
|
|
|
|
2024-06-20 03:50:26 +02:00
|
|
|
# Validate the armature animation
|
|
|
|
prim = stage.GetPrimAtPath("/root/Armature/Armature")
|
|
|
|
self.assertEqual(prim.GetTypeName(), "Skeleton")
|
|
|
|
prim_skel = UsdSkel.BindingAPI(prim)
|
|
|
|
anim = UsdSkel.Animation(prim_skel.GetAnimationSource())
|
2024-12-04 23:43:07 +01:00
|
|
|
self.assertEqual(anim.GetPrim().GetName(), "ArmatureAction_001")
|
2024-06-20 03:50:26 +02:00
|
|
|
self.assertEqual(anim.GetJointsAttr().Get(),
|
|
|
|
['Bone',
|
|
|
|
'Bone/Bone_001',
|
|
|
|
'Bone/Bone_001/Bone_002',
|
|
|
|
'Bone/Bone_001/Bone_002/Bone_003',
|
|
|
|
'Bone/Bone_001/Bone_002/Bone_003/Bone_004'])
|
|
|
|
loc_samples = anim.GetTranslationsAttr().GetTimeSamples()
|
|
|
|
rot_samples = anim.GetRotationsAttr().GetTimeSamples()
|
|
|
|
scale_samples = anim.GetScalesAttr().GetTimeSamples()
|
2024-12-07 00:15:08 +01:00
|
|
|
self.assertEqual(loc_samples, [])
|
|
|
|
self.assertEqual(rot_samples, [1.0, 2.0, 3.0])
|
|
|
|
self.assertEqual(scale_samples, [])
|
2024-06-20 03:50:26 +02:00
|
|
|
|
|
|
|
# Validate the shape key animation
|
|
|
|
prim = stage.GetPrimAtPath("/root/cube_anim_keys")
|
|
|
|
self.assertEqual(prim.GetTypeName(), "SkelRoot")
|
|
|
|
prim_skel = UsdSkel.BindingAPI(prim.GetPrimAtPath("cube_anim_keys"))
|
|
|
|
self.assertEqual(prim_skel.GetBlendShapesAttr().Get(), ['Key_1'])
|
|
|
|
prim_skel = UsdSkel.BindingAPI(prim.GetPrimAtPath("Skel"))
|
|
|
|
anim = UsdSkel.Animation(prim_skel.GetAnimationSource())
|
|
|
|
weight_samples = anim.GetBlendShapeWeightsAttr().GetTimeSamples()
|
|
|
|
self.assertEqual(weight_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
|
|
|
|
|
2025-05-16 22:12:48 +02:00
|
|
|
def test_export_text(self):
|
|
|
|
"""Test various forms of Text/Font export."""
|
|
|
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_text_test.blend"))
|
|
|
|
|
|
|
|
export_path = str(self.tempdir / "usd_text_test.usda")
|
|
|
|
self.export_and_validate(filepath=export_path, export_animation=True, evaluation_mode="RENDER")
|
|
|
|
|
|
|
|
stats = UsdUtils.ComputeUsdStageStats(export_path)
|
|
|
|
stage = Usd.Stage.Open(export_path)
|
|
|
|
|
|
|
|
# There should be 4 meshes in the output
|
|
|
|
self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 4)
|
|
|
|
|
|
|
|
bboxcache_frame1 = UsdGeom.BBoxCache(1.0, [UsdGeom.Tokens.default_])
|
|
|
|
bboxcache_frame5 = UsdGeom.BBoxCache(5.0, [UsdGeom.Tokens.default_])
|
|
|
|
|
|
|
|
# Static, flat, text
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/static/static"))
|
|
|
|
bounds1 = bboxcache_frame1.ComputeWorldBound(mesh.GetPrim())
|
|
|
|
bbox1 = bounds1.GetRange().GetMax() - bounds1.GetRange().GetMin()
|
|
|
|
self.assertEqual(mesh.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(mesh.GetExtentAttr().GetTimeSamples(), [])
|
|
|
|
self.assertTrue(bbox1[0] > 0.0)
|
|
|
|
self.assertTrue(bbox1[1] > 0.0)
|
|
|
|
self.assertAlmostEqual(bbox1[2], 0.0, 5)
|
|
|
|
|
|
|
|
# Dynamic, flat, text
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/dynamic/dynamic"))
|
|
|
|
bounds1 = bboxcache_frame1.ComputeWorldBound(mesh.GetPrim())
|
|
|
|
bounds5 = bboxcache_frame5.ComputeWorldBound(mesh.GetPrim())
|
|
|
|
bbox1 = bounds1.GetRange().GetMax() - bounds1.GetRange().GetMin()
|
|
|
|
bbox5 = bounds5.GetRange().GetMax() - bounds5.GetRange().GetMin()
|
|
|
|
self.assertEqual(mesh.GetPointsAttr().GetTimeSamples(), [1.0, 2.0, 3.0, 4.0, 5.0])
|
|
|
|
self.assertEqual(mesh.GetExtentAttr().GetTimeSamples(), [1.0, 2.0, 3.0, 4.0, 5.0])
|
|
|
|
self.assertEqual(bbox1[2], 0.0)
|
|
|
|
self.assertTrue(bbox1[0] < bbox5[0]) # Text grows on x-axis
|
|
|
|
self.assertAlmostEqual(bbox1[1], bbox5[1], 5)
|
|
|
|
self.assertAlmostEqual(bbox1[2], bbox5[2], 5)
|
|
|
|
|
|
|
|
# Static, extruded on Z, text
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/extruded/extruded"))
|
|
|
|
bounds1 = bboxcache_frame1.ComputeWorldBound(mesh.GetPrim())
|
|
|
|
bbox1 = bounds1.GetRange().GetMax() - bounds1.GetRange().GetMin()
|
|
|
|
self.assertEqual(mesh.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(mesh.GetExtentAttr().GetTimeSamples(), [])
|
|
|
|
self.assertTrue(bbox1[0] > 0.0)
|
|
|
|
self.assertTrue(bbox1[1] > 0.0)
|
|
|
|
self.assertAlmostEqual(bbox1[2], 0.1, 5)
|
|
|
|
|
|
|
|
# Static, uses depth, text
|
|
|
|
mesh = UsdGeom.Mesh(stage.GetPrimAtPath("/root/has_depth/has_depth"))
|
|
|
|
bounds1 = bboxcache_frame1.ComputeWorldBound(mesh.GetPrim())
|
|
|
|
bbox1 = bounds1.GetRange().GetMax() - bounds1.GetRange().GetMin()
|
|
|
|
self.assertEqual(mesh.GetPointsAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(mesh.GetExtentAttr().GetTimeSamples(), [])
|
|
|
|
self.assertTrue(bbox1[0] > 0.0)
|
|
|
|
self.assertTrue(bbox1[1] > 0.0)
|
|
|
|
self.assertAlmostEqual(bbox1[2], 0.1, 5)
|
|
|
|
|
2024-10-30 18:29:35 +01:00
|
|
|
def test_export_volumes(self):
|
|
|
|
"""Test various combinations of volume export including with all supported volume modifiers."""
|
|
|
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_volumes.blend"))
|
|
|
|
# Ensure the simulation zone data is baked for all relevant frames...
|
|
|
|
for frame in range(4, 15):
|
|
|
|
bpy.context.scene.frame_set(frame)
|
|
|
|
bpy.context.scene.frame_set(4)
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_volumes.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
# Validate that we see some form of time varyability across the Volume prim's extents and
|
|
|
|
# file paths. The data should be sparse so it should only be written on the frames which
|
|
|
|
# change.
|
|
|
|
|
|
|
|
# File sequence
|
|
|
|
vol_fileseq = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence"))
|
|
|
|
density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence/density_noise"))
|
|
|
|
flame = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_filesequence/vol_filesequence/flame_noise"))
|
|
|
|
self.assertEqual(vol_fileseq.GetExtentAttr().GetTimeSamples(), [10.0, 11.0])
|
|
|
|
self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(density.GetFilePathAttr().GetTimeSamples(), [8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
|
|
|
|
self.assertEqual(flame.GetFieldNameAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(flame.GetFilePathAttr().GetTimeSamples(), [8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
|
|
|
|
|
|
|
|
# Mesh To Volume
|
|
|
|
vol_mesh2vol = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_mesh2vol/vol_mesh2vol"))
|
|
|
|
density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_mesh2vol/vol_mesh2vol/density"))
|
|
|
|
self.assertEqual(vol_mesh2vol.GetExtentAttr().GetTimeSamples(),
|
|
|
|
[6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
|
|
|
self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(density.GetFilePathAttr().GetTimeSamples(),
|
|
|
|
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
|
|
|
|
|
|
|
# Volume Displace
|
|
|
|
vol_displace = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_displace/vol_displace"))
|
|
|
|
unnamed = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_displace/vol_displace/_"))
|
|
|
|
self.assertEqual(vol_displace.GetExtentAttr().GetTimeSamples(),
|
|
|
|
[5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0])
|
|
|
|
self.assertEqual(unnamed.GetFieldNameAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(unnamed.GetFilePathAttr().GetTimeSamples(),
|
|
|
|
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
|
|
|
|
|
|
|
# Geometry Node simulation
|
|
|
|
vol_sim = UsdVol.Volume(stage.GetPrimAtPath("/root/vol_sim/Volume"))
|
|
|
|
density = UsdVol.OpenVDBAsset(stage.GetPrimAtPath("/root/vol_sim/Volume/density"))
|
|
|
|
self.assertEqual(vol_sim.GetExtentAttr().GetTimeSamples(),
|
|
|
|
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
|
|
|
self.assertEqual(density.GetFieldNameAttr().GetTimeSamples(), [])
|
|
|
|
self.assertEqual(density.GetFilePathAttr().GetTimeSamples(),
|
|
|
|
[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0])
|
|
|
|
|
2024-08-24 06:58:27 +02:00
|
|
|
def test_export_xform_ops(self):
|
|
|
|
"""Test exporting different xform operation modes."""
|
|
|
|
|
|
|
|
# Create a simple scene and export using each of our xform op modes
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
|
|
|
loc = [1, 2, 3]
|
|
|
|
rot = [math.pi / 4, 0, math.pi / 8]
|
|
|
|
scale = [1, 2, 3]
|
|
|
|
|
|
|
|
bpy.ops.mesh.primitive_plane_add(location=loc, rotation=rot)
|
|
|
|
bpy.data.objects[0].scale = scale
|
|
|
|
|
|
|
|
test_path1 = self.tempdir / "temp_xform_trs_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(test_path1), xform_op_mode='TRS')
|
2024-08-24 06:58:27 +02:00
|
|
|
|
|
|
|
test_path2 = self.tempdir / "temp_xform_tos_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(test_path2), xform_op_mode='TOS')
|
2024-08-24 06:58:27 +02:00
|
|
|
|
|
|
|
test_path3 = self.tempdir / "temp_xform_mat_test.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(test_path3), xform_op_mode='MAT')
|
2024-08-24 06:58:27 +02:00
|
|
|
|
|
|
|
# Validate relevant details for each case
|
|
|
|
stage = Usd.Stage.Open(str(test_path1))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
|
|
|
|
rot_degs = [math.degrees(rot[0]), math.degrees(rot[1]), math.degrees(rot[2])]
|
|
|
|
self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:translate', 'xformOp:rotateXYZ', 'xformOp:scale'])
|
|
|
|
self.assertEqual(self.round_vector(xf.GetTranslateOp().Get()), loc)
|
|
|
|
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), rot_degs)
|
|
|
|
self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(test_path2))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
|
|
|
|
orient_quat = xf.GetOrientOp().Get()
|
|
|
|
self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:translate', 'xformOp:orient', 'xformOp:scale'])
|
|
|
|
self.assertEqual(self.round_vector(xf.GetTranslateOp().Get()), loc)
|
|
|
|
self.assertEqual(round(orient_quat.GetReal(), 4), 0.9061)
|
|
|
|
self.assertEqual(self.round_vector(orient_quat.GetImaginary()), [0.3753, 0.0747, 0.1802])
|
|
|
|
self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(test_path3))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root/Plane"))
|
|
|
|
mat = xf.GetTransformOp().Get()
|
|
|
|
mat = [
|
|
|
|
self.round_vector(mat[0]), self.round_vector(mat[1]), self.round_vector(mat[2]), self.round_vector(mat[3])
|
|
|
|
]
|
|
|
|
expected = [
|
|
|
|
[0.9239, 0.3827, 0.0, 0.0],
|
|
|
|
[-0.5412, 1.3066, 1.4142, 0.0],
|
|
|
|
[0.8118, -1.9598, 2.1213, 0.0],
|
|
|
|
[1.0, 2.0, 3.0, 1.0]
|
|
|
|
]
|
|
|
|
self.assertEqual(xf.GetXformOpOrderAttr().Get(), ['xformOp:transform'])
|
|
|
|
self.assertEqual(mat, expected)
|
|
|
|
|
|
|
|
def test_export_orientation(self):
|
|
|
|
"""Test exporting different orientation configurations."""
|
|
|
|
|
2025-05-27 05:39:02 +02:00
|
|
|
# Using the empty scene is fine for checking Stage metadata
|
2024-08-24 06:58:27 +02:00
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
|
|
|
|
|
|
|
test_path1 = self.tempdir / "temp_orientation_yup.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(
|
2024-08-24 06:58:27 +02:00
|
|
|
filepath=str(test_path1),
|
|
|
|
convert_orientation=True,
|
|
|
|
export_global_forward_selection='NEGATIVE_Z',
|
|
|
|
export_global_up_selection='Y')
|
|
|
|
|
|
|
|
test_path2 = self.tempdir / "temp_orientation_zup_rev.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(
|
2024-08-24 06:58:27 +02:00
|
|
|
filepath=str(test_path2),
|
|
|
|
convert_orientation=True,
|
|
|
|
export_global_forward_selection='NEGATIVE_Y',
|
|
|
|
export_global_up_selection='Z')
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(test_path1))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
|
|
|
|
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [-90, 0, 0])
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(test_path2))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
|
|
|
|
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [0, 0, 180])
|
|
|
|
|
2025-05-27 05:39:02 +02:00
|
|
|
# Check one final orientation using no /root xform at all (it's a different code path)
|
|
|
|
bpy.ops.mesh.primitive_cube_add()
|
|
|
|
|
|
|
|
test_path3 = self.tempdir / "temp_orientation_non_root.usda"
|
|
|
|
self.export_and_validate(filepath=str(test_path3), convert_orientation=True, root_prim_path="")
|
|
|
|
stage = Usd.Stage.Open(str(test_path3))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/Cube"))
|
|
|
|
self.assertEqual(self.round_vector(xf.GetRotateXYZOp().Get()), [-90, 0, 0])
|
|
|
|
|
USD: Add MaterialX shader export
This change adds the ability to export MaterialX networks into the resulting
USD layer.
Details:
A new export option has been added to the USD export to enable MaterialX
export. It is off by default currently due to reasons in the caveats
section.
When enabled, it exports the MaterialX shading network alongside the
UsdPreviewSurface network, on the same USD Material. This allows the same
material to be used by renderers that don't support MaterialX, using the
USDPreviewSurface as a fallback. This is similar to setups in other DCC
packages, and matches the format we've used in our Reality Composer Pro
asset library.
It uses the existing MaterialX framework used to generate MaterialX
documents for rendering, to act as the basis for the USD graph. In this
process it also re-uses the existing texture export code as well if provided
and necessary.
Once the MaterialX document is created, use usdMtlx to generate a USD
shading network. Unfortunately, usdMtlx generates a graph that is unlike
what other DCCs that support MaterialX-embedded-in-USD generates. It
generates several extra prim hierarchies, and externalizes all shader
inputs, making them difficult to edit in other MaterialX graph editors.
To workaround this, generate the MaterialX shading network onto a
temporary stage, where we then run various pre-processing steps to prevent
prim collisions and to reflow the paths once they're converted.
The PrimSpecs are then copied over to their new path. The resulting prim
hierarchy matches what many artists we've worked with prefer to work with.
Caveats:
The Export MaterialX check is off by default. When using the Principled
BSDF, the resulting graph is very usable. However, when using some of the
other BSDFs, the shading networks generated by the existing MaterialX
framework in Blender generate some shading graphs that are difficult for
usdview and other DCC's to understand. The graph is still correct, but
because we're trying to prioritize compatibility, the default is off.
In future PRs we can aim to make the graphs for those other BSDFs play
better with other DCCs.
Other Implementation Details:
As part of this commit we've also done the following:
* Place some of the materialx graphs inside a passthrough nodegraph to
avoid node conflicts.
* Better handle some shader output types , and better handle some
conflict cases.
* Moved the ExportTextureFunction to materials.h due to some difficult
to resolve header ordering issues. This has no effect on any runtime code.
* There is a test for the MaterialX export that does some basic checking to
make sure we get an export out the other end that matches our expectations
Authored by Apple: Dhruv Govil
This PR is based on an earlier implementation by Brecht van Lommel , as well
as Brian Savery and his teams' work at AMD to implement the general
MaterialX framework within Blender.
Pull Request: https://projects.blender.org/blender/blender/pulls/122575
2024-06-05 20:43:44 +02:00
|
|
|
def test_materialx_network(self):
|
|
|
|
"""Test exporting that a MaterialX export makes it out alright"""
|
2024-10-04 02:50:16 +02:00
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
|
USD: Add MaterialX shader export
This change adds the ability to export MaterialX networks into the resulting
USD layer.
Details:
A new export option has been added to the USD export to enable MaterialX
export. It is off by default currently due to reasons in the caveats
section.
When enabled, it exports the MaterialX shading network alongside the
UsdPreviewSurface network, on the same USD Material. This allows the same
material to be used by renderers that don't support MaterialX, using the
USDPreviewSurface as a fallback. This is similar to setups in other DCC
packages, and matches the format we've used in our Reality Composer Pro
asset library.
It uses the existing MaterialX framework used to generate MaterialX
documents for rendering, to act as the basis for the USD graph. In this
process it also re-uses the existing texture export code as well if provided
and necessary.
Once the MaterialX document is created, use usdMtlx to generate a USD
shading network. Unfortunately, usdMtlx generates a graph that is unlike
what other DCCs that support MaterialX-embedded-in-USD generates. It
generates several extra prim hierarchies, and externalizes all shader
inputs, making them difficult to edit in other MaterialX graph editors.
To workaround this, generate the MaterialX shading network onto a
temporary stage, where we then run various pre-processing steps to prevent
prim collisions and to reflow the paths once they're converted.
The PrimSpecs are then copied over to their new path. The resulting prim
hierarchy matches what many artists we've worked with prefer to work with.
Caveats:
The Export MaterialX check is off by default. When using the Principled
BSDF, the resulting graph is very usable. However, when using some of the
other BSDFs, the shading networks generated by the existing MaterialX
framework in Blender generate some shading graphs that are difficult for
usdview and other DCC's to understand. The graph is still correct, but
because we're trying to prioritize compatibility, the default is off.
In future PRs we can aim to make the graphs for those other BSDFs play
better with other DCCs.
Other Implementation Details:
As part of this commit we've also done the following:
* Place some of the materialx graphs inside a passthrough nodegraph to
avoid node conflicts.
* Better handle some shader output types , and better handle some
conflict cases.
* Moved the ExportTextureFunction to materials.h due to some difficult
to resolve header ordering issues. This has no effect on any runtime code.
* There is a test for the MaterialX export that does some basic checking to
make sure we get an export out the other end that matches our expectations
Authored by Apple: Dhruv Govil
This PR is based on an earlier implementation by Brecht van Lommel , as well
as Brian Savery and his teams' work at AMD to implement the general
MaterialX framework within Blender.
Pull Request: https://projects.blender.org/blender/blender/pulls/122575
2024-06-05 20:43:44 +02:00
|
|
|
export_path = self.tempdir / "materialx.usda"
|
2024-10-04 02:50:16 +02:00
|
|
|
|
|
|
|
# USD currently has an issue where embedded MaterialX graphs cause validation to fail.
|
2025-05-02 06:19:28 +02:00
|
|
|
# Note: We use the below patch for now; keep this in mind if it causes issues in the future.
|
2024-10-04 02:50:16 +02:00
|
|
|
# See: https://github.com/PixarAnimationStudios/OpenUSD/pull/3243
|
2025-05-02 06:19:28 +02:00
|
|
|
res = self.export_and_validate(
|
USD: Add MaterialX shader export
This change adds the ability to export MaterialX networks into the resulting
USD layer.
Details:
A new export option has been added to the USD export to enable MaterialX
export. It is off by default currently due to reasons in the caveats
section.
When enabled, it exports the MaterialX shading network alongside the
UsdPreviewSurface network, on the same USD Material. This allows the same
material to be used by renderers that don't support MaterialX, using the
USDPreviewSurface as a fallback. This is similar to setups in other DCC
packages, and matches the format we've used in our Reality Composer Pro
asset library.
It uses the existing MaterialX framework used to generate MaterialX
documents for rendering, to act as the basis for the USD graph. In this
process it also re-uses the existing texture export code as well if provided
and necessary.
Once the MaterialX document is created, use usdMtlx to generate a USD
shading network. Unfortunately, usdMtlx generates a graph that is unlike
what other DCCs that support MaterialX-embedded-in-USD generates. It
generates several extra prim hierarchies, and externalizes all shader
inputs, making them difficult to edit in other MaterialX graph editors.
To workaround this, generate the MaterialX shading network onto a
temporary stage, where we then run various pre-processing steps to prevent
prim collisions and to reflow the paths once they're converted.
The PrimSpecs are then copied over to their new path. The resulting prim
hierarchy matches what many artists we've worked with prefer to work with.
Caveats:
The Export MaterialX check is off by default. When using the Principled
BSDF, the resulting graph is very usable. However, when using some of the
other BSDFs, the shading networks generated by the existing MaterialX
framework in Blender generate some shading graphs that are difficult for
usdview and other DCC's to understand. The graph is still correct, but
because we're trying to prioritize compatibility, the default is off.
In future PRs we can aim to make the graphs for those other BSDFs play
better with other DCCs.
Other Implementation Details:
As part of this commit we've also done the following:
* Place some of the materialx graphs inside a passthrough nodegraph to
avoid node conflicts.
* Better handle some shader output types , and better handle some
conflict cases.
* Moved the ExportTextureFunction to materials.h due to some difficult
to resolve header ordering issues. This has no effect on any runtime code.
* There is a test for the MaterialX export that does some basic checking to
make sure we get an export out the other end that matches our expectations
Authored by Apple: Dhruv Govil
This PR is based on an earlier implementation by Brecht van Lommel , as well
as Brian Savery and his teams' work at AMD to implement the general
MaterialX framework within Blender.
Pull Request: https://projects.blender.org/blender/blender/pulls/122575
2024-06-05 20:43:44 +02:00
|
|
|
filepath=str(export_path),
|
|
|
|
export_materials=True,
|
|
|
|
generate_materialx_network=True,
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
)
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
material_prim = stage.GetPrimAtPath("/root/_materials/Material")
|
|
|
|
self.assertTrue(material_prim, "Could not find Material prim")
|
|
|
|
|
2025-04-30 19:54:17 +02:00
|
|
|
self.assertTrue(material_prim.HasAPI(UsdMtlx.MaterialXConfigAPI))
|
|
|
|
mtlx_config_api = UsdMtlx.MaterialXConfigAPI(material_prim)
|
|
|
|
mtlx_version_attr = mtlx_config_api.GetConfigMtlxVersionAttr()
|
|
|
|
self.assertTrue(mtlx_version_attr, "Could not find mtlx config version attribute")
|
|
|
|
|
USD: Add MaterialX shader export
This change adds the ability to export MaterialX networks into the resulting
USD layer.
Details:
A new export option has been added to the USD export to enable MaterialX
export. It is off by default currently due to reasons in the caveats
section.
When enabled, it exports the MaterialX shading network alongside the
UsdPreviewSurface network, on the same USD Material. This allows the same
material to be used by renderers that don't support MaterialX, using the
USDPreviewSurface as a fallback. This is similar to setups in other DCC
packages, and matches the format we've used in our Reality Composer Pro
asset library.
It uses the existing MaterialX framework used to generate MaterialX
documents for rendering, to act as the basis for the USD graph. In this
process it also re-uses the existing texture export code as well if provided
and necessary.
Once the MaterialX document is created, use usdMtlx to generate a USD
shading network. Unfortunately, usdMtlx generates a graph that is unlike
what other DCCs that support MaterialX-embedded-in-USD generates. It
generates several extra prim hierarchies, and externalizes all shader
inputs, making them difficult to edit in other MaterialX graph editors.
To workaround this, generate the MaterialX shading network onto a
temporary stage, where we then run various pre-processing steps to prevent
prim collisions and to reflow the paths once they're converted.
The PrimSpecs are then copied over to their new path. The resulting prim
hierarchy matches what many artists we've worked with prefer to work with.
Caveats:
The Export MaterialX check is off by default. When using the Principled
BSDF, the resulting graph is very usable. However, when using some of the
other BSDFs, the shading networks generated by the existing MaterialX
framework in Blender generate some shading graphs that are difficult for
usdview and other DCC's to understand. The graph is still correct, but
because we're trying to prioritize compatibility, the default is off.
In future PRs we can aim to make the graphs for those other BSDFs play
better with other DCCs.
Other Implementation Details:
As part of this commit we've also done the following:
* Place some of the materialx graphs inside a passthrough nodegraph to
avoid node conflicts.
* Better handle some shader output types , and better handle some
conflict cases.
* Moved the ExportTextureFunction to materials.h due to some difficult
to resolve header ordering issues. This has no effect on any runtime code.
* There is a test for the MaterialX export that does some basic checking to
make sure we get an export out the other end that matches our expectations
Authored by Apple: Dhruv Govil
This PR is based on an earlier implementation by Brecht van Lommel , as well
as Brian Savery and his teams' work at AMD to implement the general
MaterialX framework within Blender.
Pull Request: https://projects.blender.org/blender/blender/pulls/122575
2024-06-05 20:43:44 +02:00
|
|
|
material = UsdShade.Material(material_prim)
|
|
|
|
mtlx_output = material.GetOutput("mtlx:surface")
|
|
|
|
self.assertTrue(mtlx_output, "Could not find mtlx output")
|
|
|
|
|
|
|
|
connection, source_name, _ = UsdShade.ConnectableAPI.GetConnectedSource(
|
|
|
|
mtlx_output
|
|
|
|
) or [None, None, None]
|
|
|
|
|
|
|
|
self.assertTrue((connection and source_name), "Could not find mtlx output source")
|
|
|
|
|
|
|
|
shader = UsdShade.Shader(connection.GetPrim())
|
|
|
|
self.assertTrue(shader, "Connected prim is not a shader")
|
|
|
|
|
|
|
|
shader_id = shader.GetIdAttr().Get()
|
2025-05-15 19:11:35 +02:00
|
|
|
self.assertEqual(shader_id, "ND_open_pbr_surface_surfaceshader", "Shader is not an OpenPBR Surface")
|
USD: Add MaterialX shader export
This change adds the ability to export MaterialX networks into the resulting
USD layer.
Details:
A new export option has been added to the USD export to enable MaterialX
export. It is off by default currently due to reasons in the caveats
section.
When enabled, it exports the MaterialX shading network alongside the
UsdPreviewSurface network, on the same USD Material. This allows the same
material to be used by renderers that don't support MaterialX, using the
USDPreviewSurface as a fallback. This is similar to setups in other DCC
packages, and matches the format we've used in our Reality Composer Pro
asset library.
It uses the existing MaterialX framework used to generate MaterialX
documents for rendering, to act as the basis for the USD graph. In this
process it also re-uses the existing texture export code as well if provided
and necessary.
Once the MaterialX document is created, use usdMtlx to generate a USD
shading network. Unfortunately, usdMtlx generates a graph that is unlike
what other DCCs that support MaterialX-embedded-in-USD generates. It
generates several extra prim hierarchies, and externalizes all shader
inputs, making them difficult to edit in other MaterialX graph editors.
To workaround this, generate the MaterialX shading network onto a
temporary stage, where we then run various pre-processing steps to prevent
prim collisions and to reflow the paths once they're converted.
The PrimSpecs are then copied over to their new path. The resulting prim
hierarchy matches what many artists we've worked with prefer to work with.
Caveats:
The Export MaterialX check is off by default. When using the Principled
BSDF, the resulting graph is very usable. However, when using some of the
other BSDFs, the shading networks generated by the existing MaterialX
framework in Blender generate some shading graphs that are difficult for
usdview and other DCC's to understand. The graph is still correct, but
because we're trying to prioritize compatibility, the default is off.
In future PRs we can aim to make the graphs for those other BSDFs play
better with other DCCs.
Other Implementation Details:
As part of this commit we've also done the following:
* Place some of the materialx graphs inside a passthrough nodegraph to
avoid node conflicts.
* Better handle some shader output types , and better handle some
conflict cases.
* Moved the ExportTextureFunction to materials.h due to some difficult
to resolve header ordering issues. This has no effect on any runtime code.
* There is a test for the MaterialX export that does some basic checking to
make sure we get an export out the other end that matches our expectations
Authored by Apple: Dhruv Govil
This PR is based on an earlier implementation by Brecht van Lommel , as well
as Brian Savery and his teams' work at AMD to implement the general
MaterialX framework within Blender.
Pull Request: https://projects.blender.org/blender/blender/pulls/122575
2024-06-05 20:43:44 +02:00
|
|
|
|
2024-08-27 05:38:55 +02:00
|
|
|
def test_hooks(self):
|
|
|
|
"""Validate USD Hook integration for both import and export"""
|
|
|
|
|
|
|
|
# Create a simple scene with 1 object and 1 material
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
|
|
|
material = bpy.data.materials.new(name="test_material")
|
|
|
|
material.use_nodes = True
|
|
|
|
bpy.ops.mesh.primitive_plane_add()
|
|
|
|
bpy.data.objects[0].data.materials.append(material)
|
|
|
|
|
|
|
|
# Register both USD hooks
|
|
|
|
bpy.utils.register_class(USDHook1)
|
|
|
|
bpy.utils.register_class(USDHook2)
|
|
|
|
|
|
|
|
# Instruct them to do various actions inside their implementation
|
|
|
|
USDHookBase.instructions = {
|
|
|
|
"on_material_export": ["return False", "return True"],
|
|
|
|
"on_export": ["throw", "return True"],
|
|
|
|
"on_import": ["throw", "return True"],
|
|
|
|
}
|
|
|
|
|
|
|
|
USDHookBase.responses = {
|
|
|
|
"on_material_export": [],
|
|
|
|
"on_export": [],
|
|
|
|
"on_import": [],
|
|
|
|
}
|
|
|
|
|
|
|
|
test_path = self.tempdir / "hook.usda"
|
|
|
|
|
|
|
|
try:
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(test_path))
|
2024-08-27 05:38:55 +02:00
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
bpy.ops.wm.usd_import(filepath=str(test_path))
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# Unregister the hooks. We do this here in case the following asserts fail.
|
|
|
|
bpy.utils.unregister_class(USDHook1)
|
|
|
|
bpy.utils.unregister_class(USDHook2)
|
|
|
|
|
|
|
|
# Validate that the Hooks executed and responded accordingly...
|
|
|
|
self.assertEqual(USDHookBase.responses["on_material_export"], ["returned False", "returned True"])
|
|
|
|
self.assertEqual(USDHookBase.responses["on_export"], ["threw exception", "returned True"])
|
|
|
|
self.assertEqual(USDHookBase.responses["on_import"], ["threw exception", "returned True"])
|
|
|
|
|
|
|
|
# Now that the hooks are unregistered they should not be executed for import and export.
|
|
|
|
USDHookBase.responses = {
|
|
|
|
"on_material_export": [],
|
|
|
|
"on_export": [],
|
|
|
|
"on_import": [],
|
|
|
|
}
|
2024-10-04 02:50:16 +02:00
|
|
|
self.export_and_validate(filepath=str(test_path))
|
|
|
|
self.export_and_validate(filepath=str(test_path))
|
2024-08-27 05:38:55 +02:00
|
|
|
self.assertEqual(USDHookBase.responses["on_material_export"], [])
|
|
|
|
self.assertEqual(USDHookBase.responses["on_export"], [])
|
|
|
|
self.assertEqual(USDHookBase.responses["on_import"], [])
|
|
|
|
|
USD: Add option to merge transform and shape on export
Adds the option `merge_parent_xform` to the USD export operator and
panels so the transform and shapes are merged into a single USD Prim.
Without the option (existing default), a top-level mesh would be
exported as a top-level `Xform` that has a `Mesh` child:
```
def Xform "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
def Mesh "MyBlenderMeshData"
{
}
}
```
This matches the Blender data model, where a transformable object
contains a geometric shape (like a mesh). This structure is also very
valid in USD, where we don't want to directly instantiate geometric
primitives[1]
However, "since number of prims on a stage is one of the primary factors
that governs how USD scale"[2], to reduce the number of prims in a
stage, geometric primitives *are transformable* themselves (see the
inheritence diagram[3]).
As such, the new export option allows to export geometric primitives
without the parent transform:
```
def Mesh "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
}
```
This MR adds a the `is_object_data_context` flag to the
`HierarchyContext` context structure. The point of this change is to
make unambiguous in `USDHierarchyIterator::create_usd_export_context`
the fact that an `object` or a `data` is currently being exported: the
new `merge_parent_xform` option is meaningless for `object`. Only `data`
can be exported with a parent `Xform` or as a child of said `Xform`.
Ideally this flag would not be needed at all: the final USD prim path
*could* be computed in an override of the virtual
`AbstractHierarchyIterator::get_object_data_path` method. However, this
would mean that an `object` and a `data` would have the same export path.
This does not currently play well with
`AbstractHierarchyIterator::ensure_writer`, where `writers` are cached
*by their export path*: it would cache a transform writer, but will skip
the subsequent data writer.
Additionally, another new `is_parent` flag is added to handle the case
where merging the Xform is invalid: objects that are parents to other
objects should remain unmerged as otherwise that would yield invalid USD
files.
[1] https://openusd.org/release/glossary.html#usdglossary-gprim
[2] https://openusd.org/release/glossary.html#usdglossary-instancing
[3] https://openusd.org/release/api/class_usd_geom_xformable.html
Co-authored-by: Odréanne Breton <odreanne.breton@ubisoft.com>
Co-authored-by: Sttevan Carnali Joga <sttevan.carnali-joga@ubisoft.com>
Co-authored-by: Charles Flèche <charles.fleche@ubisoft.com>
2024-11-19 19:18:53 +01:00
|
|
|
def test_merge_parent_xform_false(self):
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_export_test.blend"))
|
|
|
|
|
|
|
|
test_path = self.tempdir / "test_merge_parent_xform_false.usda"
|
|
|
|
|
|
|
|
self.export_and_validate(filepath=str(test_path), merge_parent_xform=False)
|
|
|
|
|
|
|
|
expected = (
|
|
|
|
("/root", "Xform"),
|
|
|
|
("/root/Dupli1", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/Face", "Mesh"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2/Ear", "Mesh"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1/Ear", "Mesh"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Nose_3", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Nose_3/Nose", "Mesh"),
|
|
|
|
("/root/_materials", "Scope"),
|
|
|
|
("/root/_materials/Head", "Material"),
|
|
|
|
("/root/_materials/Head/Principled_BSDF", "Shader"),
|
|
|
|
("/root/_materials/Nose", "Material"),
|
|
|
|
("/root/_materials/Nose/Principled_BSDF", "Shader"),
|
|
|
|
("/root/ParentOfDupli2", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Icosphere", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/Face", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1/Ear", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2/Ear", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3/Nose", "Mesh"),
|
|
|
|
("/root/Ground_plane", "Xform"),
|
|
|
|
("/root/Ground_plane/Plane", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R/Ear", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose/Nose", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L/Ear", "Mesh"),
|
|
|
|
("/root/Camera", "Xform"),
|
|
|
|
("/root/Camera/Camera", "Camera"),
|
|
|
|
("/root/env_light", "DomeLight")
|
|
|
|
)
|
2025-03-21 21:29:08 +01:00
|
|
|
expected = tuple(sorted(expected, key=lambda pair: pair[0]))
|
USD: Add option to merge transform and shape on export
Adds the option `merge_parent_xform` to the USD export operator and
panels so the transform and shapes are merged into a single USD Prim.
Without the option (existing default), a top-level mesh would be
exported as a top-level `Xform` that has a `Mesh` child:
```
def Xform "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
def Mesh "MyBlenderMeshData"
{
}
}
```
This matches the Blender data model, where a transformable object
contains a geometric shape (like a mesh). This structure is also very
valid in USD, where we don't want to directly instantiate geometric
primitives[1]
However, "since number of prims on a stage is one of the primary factors
that governs how USD scale"[2], to reduce the number of prims in a
stage, geometric primitives *are transformable* themselves (see the
inheritence diagram[3]).
As such, the new export option allows to export geometric primitives
without the parent transform:
```
def Mesh "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
}
```
This MR adds a the `is_object_data_context` flag to the
`HierarchyContext` context structure. The point of this change is to
make unambiguous in `USDHierarchyIterator::create_usd_export_context`
the fact that an `object` or a `data` is currently being exported: the
new `merge_parent_xform` option is meaningless for `object`. Only `data`
can be exported with a parent `Xform` or as a child of said `Xform`.
Ideally this flag would not be needed at all: the final USD prim path
*could* be computed in an override of the virtual
`AbstractHierarchyIterator::get_object_data_path` method. However, this
would mean that an `object` and a `data` would have the same export path.
This does not currently play well with
`AbstractHierarchyIterator::ensure_writer`, where `writers` are cached
*by their export path*: it would cache a transform writer, but will skip
the subsequent data writer.
Additionally, another new `is_parent` flag is added to handle the case
where merging the Xform is invalid: objects that are parents to other
objects should remain unmerged as otherwise that would yield invalid USD
files.
[1] https://openusd.org/release/glossary.html#usdglossary-gprim
[2] https://openusd.org/release/glossary.html#usdglossary-instancing
[3] https://openusd.org/release/api/class_usd_geom_xformable.html
Co-authored-by: Odréanne Breton <odreanne.breton@ubisoft.com>
Co-authored-by: Sttevan Carnali Joga <sttevan.carnali-joga@ubisoft.com>
Co-authored-by: Charles Flèche <charles.fleche@ubisoft.com>
2024-11-19 19:18:53 +01:00
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(test_path))
|
|
|
|
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
|
2025-03-21 21:29:08 +01:00
|
|
|
actual = tuple(sorted(actual, key=lambda pair: pair[0]))
|
USD: Add option to merge transform and shape on export
Adds the option `merge_parent_xform` to the USD export operator and
panels so the transform and shapes are merged into a single USD Prim.
Without the option (existing default), a top-level mesh would be
exported as a top-level `Xform` that has a `Mesh` child:
```
def Xform "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
def Mesh "MyBlenderMeshData"
{
}
}
```
This matches the Blender data model, where a transformable object
contains a geometric shape (like a mesh). This structure is also very
valid in USD, where we don't want to directly instantiate geometric
primitives[1]
However, "since number of prims on a stage is one of the primary factors
that governs how USD scale"[2], to reduce the number of prims in a
stage, geometric primitives *are transformable* themselves (see the
inheritence diagram[3]).
As such, the new export option allows to export geometric primitives
without the parent transform:
```
def Mesh "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
}
```
This MR adds a the `is_object_data_context` flag to the
`HierarchyContext` context structure. The point of this change is to
make unambiguous in `USDHierarchyIterator::create_usd_export_context`
the fact that an `object` or a `data` is currently being exported: the
new `merge_parent_xform` option is meaningless for `object`. Only `data`
can be exported with a parent `Xform` or as a child of said `Xform`.
Ideally this flag would not be needed at all: the final USD prim path
*could* be computed in an override of the virtual
`AbstractHierarchyIterator::get_object_data_path` method. However, this
would mean that an `object` and a `data` would have the same export path.
This does not currently play well with
`AbstractHierarchyIterator::ensure_writer`, where `writers` are cached
*by their export path*: it would cache a transform writer, but will skip
the subsequent data writer.
Additionally, another new `is_parent` flag is added to handle the case
where merging the Xform is invalid: objects that are parents to other
objects should remain unmerged as otherwise that would yield invalid USD
files.
[1] https://openusd.org/release/glossary.html#usdglossary-gprim
[2] https://openusd.org/release/glossary.html#usdglossary-instancing
[3] https://openusd.org/release/api/class_usd_geom_xformable.html
Co-authored-by: Odréanne Breton <odreanne.breton@ubisoft.com>
Co-authored-by: Sttevan Carnali Joga <sttevan.carnali-joga@ubisoft.com>
Co-authored-by: Charles Flèche <charles.fleche@ubisoft.com>
2024-11-19 19:18:53 +01:00
|
|
|
|
|
|
|
self.assertTupleEqual(expected, actual)
|
|
|
|
|
|
|
|
def test_merge_parent_xform_true(self):
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_export_test.blend"))
|
|
|
|
|
|
|
|
test_path = self.tempdir / "test_merge_parent_xform_true.usda"
|
|
|
|
|
|
|
|
self.export_and_validate(filepath=str(test_path), merge_parent_xform=True)
|
|
|
|
|
|
|
|
expected = (
|
|
|
|
("/root", "Xform"),
|
|
|
|
("/root/Dupli1", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0", "Xform"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/Face", "Mesh"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Ear_R_2", "Mesh"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Ear_L_1", "Mesh"),
|
|
|
|
("/root/Dupli1/GEO_Head_0/GEO_Nose_3", "Mesh"),
|
|
|
|
("/root/_materials", "Scope"),
|
|
|
|
("/root/_materials/Head", "Material"),
|
|
|
|
("/root/_materials/Head/Principled_BSDF", "Shader"),
|
|
|
|
("/root/_materials/Nose", "Material"),
|
|
|
|
("/root/_materials/Nose/Principled_BSDF", "Shader"),
|
|
|
|
("/root/ParentOfDupli2", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Icosphere", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0", "Xform"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/Face", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_L_1", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Ear_R_2", "Mesh"),
|
|
|
|
("/root/ParentOfDupli2/Dupli2/GEO_Head_0/GEO_Nose_3", "Mesh"),
|
|
|
|
("/root/Ground_plane", "Xform"),
|
|
|
|
("/root/Ground_plane/Plane", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head", "Xform"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/Face", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_R", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Nose", "Mesh"),
|
|
|
|
("/root/Ground_plane/OutsideDupliGrandParent/OutsideDupliParent/GEO_Head/GEO_Ear_L", "Mesh"),
|
|
|
|
("/root/Camera", "Camera"),
|
|
|
|
("/root/env_light", "DomeLight")
|
|
|
|
)
|
|
|
|
|
2025-03-21 21:29:08 +01:00
|
|
|
expected = tuple(sorted(expected, key=lambda pair: pair[0]))
|
USD: Add option to merge transform and shape on export
Adds the option `merge_parent_xform` to the USD export operator and
panels so the transform and shapes are merged into a single USD Prim.
Without the option (existing default), a top-level mesh would be
exported as a top-level `Xform` that has a `Mesh` child:
```
def Xform "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
def Mesh "MyBlenderMeshData"
{
}
}
```
This matches the Blender data model, where a transformable object
contains a geometric shape (like a mesh). This structure is also very
valid in USD, where we don't want to directly instantiate geometric
primitives[1]
However, "since number of prims on a stage is one of the primary factors
that governs how USD scale"[2], to reduce the number of prims in a
stage, geometric primitives *are transformable* themselves (see the
inheritence diagram[3]).
As such, the new export option allows to export geometric primitives
without the parent transform:
```
def Mesh "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
}
```
This MR adds a the `is_object_data_context` flag to the
`HierarchyContext` context structure. The point of this change is to
make unambiguous in `USDHierarchyIterator::create_usd_export_context`
the fact that an `object` or a `data` is currently being exported: the
new `merge_parent_xform` option is meaningless for `object`. Only `data`
can be exported with a parent `Xform` or as a child of said `Xform`.
Ideally this flag would not be needed at all: the final USD prim path
*could* be computed in an override of the virtual
`AbstractHierarchyIterator::get_object_data_path` method. However, this
would mean that an `object` and a `data` would have the same export path.
This does not currently play well with
`AbstractHierarchyIterator::ensure_writer`, where `writers` are cached
*by their export path*: it would cache a transform writer, but will skip
the subsequent data writer.
Additionally, another new `is_parent` flag is added to handle the case
where merging the Xform is invalid: objects that are parents to other
objects should remain unmerged as otherwise that would yield invalid USD
files.
[1] https://openusd.org/release/glossary.html#usdglossary-gprim
[2] https://openusd.org/release/glossary.html#usdglossary-instancing
[3] https://openusd.org/release/api/class_usd_geom_xformable.html
Co-authored-by: Odréanne Breton <odreanne.breton@ubisoft.com>
Co-authored-by: Sttevan Carnali Joga <sttevan.carnali-joga@ubisoft.com>
Co-authored-by: Charles Flèche <charles.fleche@ubisoft.com>
2024-11-19 19:18:53 +01:00
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(test_path))
|
|
|
|
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
|
2025-03-21 21:29:08 +01:00
|
|
|
actual = tuple(sorted(actual, key=lambda pair: pair[0]))
|
USD: Add option to merge transform and shape on export
Adds the option `merge_parent_xform` to the USD export operator and
panels so the transform and shapes are merged into a single USD Prim.
Without the option (existing default), a top-level mesh would be
exported as a top-level `Xform` that has a `Mesh` child:
```
def Xform "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
def Mesh "MyBlenderMeshData"
{
}
}
```
This matches the Blender data model, where a transformable object
contains a geometric shape (like a mesh). This structure is also very
valid in USD, where we don't want to directly instantiate geometric
primitives[1]
However, "since number of prims on a stage is one of the primary factors
that governs how USD scale"[2], to reduce the number of prims in a
stage, geometric primitives *are transformable* themselves (see the
inheritence diagram[3]).
As such, the new export option allows to export geometric primitives
without the parent transform:
```
def Mesh "MyBlenderMeshObject"
{
matrix4d xformOp:transform = ...
}
```
This MR adds a the `is_object_data_context` flag to the
`HierarchyContext` context structure. The point of this change is to
make unambiguous in `USDHierarchyIterator::create_usd_export_context`
the fact that an `object` or a `data` is currently being exported: the
new `merge_parent_xform` option is meaningless for `object`. Only `data`
can be exported with a parent `Xform` or as a child of said `Xform`.
Ideally this flag would not be needed at all: the final USD prim path
*could* be computed in an override of the virtual
`AbstractHierarchyIterator::get_object_data_path` method. However, this
would mean that an `object` and a `data` would have the same export path.
This does not currently play well with
`AbstractHierarchyIterator::ensure_writer`, where `writers` are cached
*by their export path*: it would cache a transform writer, but will skip
the subsequent data writer.
Additionally, another new `is_parent` flag is added to handle the case
where merging the Xform is invalid: objects that are parents to other
objects should remain unmerged as otherwise that would yield invalid USD
files.
[1] https://openusd.org/release/glossary.html#usdglossary-gprim
[2] https://openusd.org/release/glossary.html#usdglossary-instancing
[3] https://openusd.org/release/api/class_usd_geom_xformable.html
Co-authored-by: Odréanne Breton <odreanne.breton@ubisoft.com>
Co-authored-by: Sttevan Carnali Joga <sttevan.carnali-joga@ubisoft.com>
Co-authored-by: Charles Flèche <charles.fleche@ubisoft.com>
2024-11-19 19:18:53 +01:00
|
|
|
|
|
|
|
self.assertTupleEqual(expected, actual)
|
|
|
|
|
2025-01-02 22:04:58 +01:00
|
|
|
def test_export_units(self):
|
|
|
|
"""Test specifying stage meters per unit on export."""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend"))
|
|
|
|
|
2025-05-27 05:39:02 +02:00
|
|
|
# Check all unit conversions we support
|
2025-01-29 20:03:36 +01:00
|
|
|
units = (
|
|
|
|
("mm", 'MILLIMETERS', 0.001), ("cm", 'CENTIMETERS', 0.01), ("km", 'KILOMETERS', 1000),
|
|
|
|
("in", 'INCHES', 0.0254), ("ft", 'FEET', 0.3048), ("yd", 'YARDS', 0.9144),
|
|
|
|
("default", "", 1), ("custom", 'CUSTOM', 0.125)
|
2025-01-02 22:04:58 +01:00
|
|
|
)
|
2025-01-29 20:03:36 +01:00
|
|
|
for name, unit, value in units:
|
|
|
|
export_path = self.tempdir / f"usd_export_units_test_{name}.usda"
|
|
|
|
if name == "default":
|
|
|
|
self.export_and_validate(filepath=str(export_path))
|
|
|
|
elif name == "custom":
|
|
|
|
self.export_and_validate(filepath=str(export_path), convert_scene_units=unit, meters_per_unit=value)
|
|
|
|
else:
|
|
|
|
self.export_and_validate(filepath=str(export_path), convert_scene_units=unit)
|
2025-01-02 22:04:58 +01:00
|
|
|
|
2025-05-27 05:39:02 +02:00
|
|
|
# Verify that the Stage meters per unit metadata is set correctly
|
2025-01-29 20:03:36 +01:00
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
self.assertEqual(UsdGeom.GetStageMetersPerUnit(stage), value)
|
2025-01-02 22:04:58 +01:00
|
|
|
|
2025-05-27 05:39:02 +02:00
|
|
|
# Verify that the /root xform has the expected scale (the default case should be empty)
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/root"))
|
|
|
|
if name == "default":
|
|
|
|
self.assertFalse(xf.GetScaleOp().GetAttr().IsValid())
|
|
|
|
else:
|
|
|
|
scale = self.round_vector([1.0 / value] * 3)
|
|
|
|
self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), scale)
|
|
|
|
|
|
|
|
# Check one final unit conversion using no /root xform at all (it's a different code path)
|
|
|
|
bpy.ops.mesh.primitive_cube_add()
|
|
|
|
|
|
|
|
export_path = self.tempdir / f"usd_export_units_test_non_root.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path), convert_scene_units="CENTIMETERS", root_prim_path="")
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
xf = UsdGeom.Xformable(stage.GetPrimAtPath("/Cube"))
|
|
|
|
self.assertEqual(self.round_vector(xf.GetScaleOp().Get()), [100, 100, 100])
|
|
|
|
|
USD: Scene graph instancing export
Refactored USD instancing export to support instanceable references.
With this change, it's now possible to instance object hierarchies and
geometry types other than meshes (e.g., curves, point clouds, etc.).
No longer marking mesh prims as instances in
USDGenericMeshWriter::write_mesh().
USDTransformWriter::do_write() now marks the Xform as instanceable
with a reference to the prototype's Xform when the Blender object is
an instance.
In USDAbstractWriter::mark_as_instance() the target prim is now marked
as instanceable.
Added AbstractHierarchyIterator virtual functions include_data_writers()
and include_child_writers() to allow pruning children of instanceable Xforms
in AbstractHierarchyIterator::make_writers(). These functions return true
in the base class implementation, so that the iterator behavior for Alembic
exports is unaffected. In the USDHierarchyIterator subclass, these functions
are overridden to return false if instancing is enabled and the objects are
instances.
Added virtual function AbstractHierarchyIterator::should_determine_duplication_references()
which returns true if duplication references should be resolved for children
of a given context. This function is overridden in USDHierarchyIterator to
skip processing children of instances, which is more efficient for USD export,
since children of instances are pruned during traversal for writing. For nested
instances where the original prototype is not included in the export, this also
avoids designating a duplicated object parented to an instance as "the original",
which would cause USD errors since defining a prim under an instance
proxy is not allowed.
Extended logic in `AbstractHierarchyIterator::determine_duplication_references()`
to identify prototypes.
Added new function `HierarchyContext::is_prototype()`.
Disallowing merging with parent for instances and prototypes, since
the Xforms cannot be discarded in those cases.
Extended `USDWriterAbstract::ensure_usd_material()` with special logic
to ensure materials for prototype prims are defined in the subtree of the
prototype. This helps ensure the hierarchical encapsulation requirement
for prototypes and is required by certain renderers (e.g., Houdini's Karma)
for instance materials to render.
Added a new `process_scene_graph_instances()` function to ensure
prototypes are exported as abstract prims.
Added python tests test_export_native_instancing_true and
test_export_native_instancing_false.
Pull Request: https://projects.blender.org/blender/blender/pulls/131707
2025-01-28 15:51:56 +01:00
|
|
|
def test_export_native_instancing_true(self):
|
|
|
|
"""Test exporting instanced objects to native (scne graph) instances."""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "nested_instancing_test.blend"))
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_export_nested_instancing_true.usda"
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=str(export_path),
|
|
|
|
use_instancing=True
|
|
|
|
)
|
|
|
|
|
|
|
|
# The USD should contain two instances of a plane which has two
|
|
|
|
# instances of a point cloud as children.
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
stats = UsdUtils.ComputeUsdStageStats(stage)
|
|
|
|
self.assertEqual(stats['totalInstanceCount'], 6, "Unexpected number of instances")
|
|
|
|
self.assertEqual(stats['prototypeCount'], 2, "Unexpected number of prototypes")
|
|
|
|
self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 1, "Unexpected number of primary meshes")
|
|
|
|
self.assertEqual(stats['primary']['primCountsByType']['Points'], 1, "Unexpected number of primary point clouds")
|
|
|
|
self.assertEqual(stats['prototypes']['primCountsByType']['Mesh'], 1, "Unexpected number of prototype meshes")
|
|
|
|
self.assertEqual(stats['prototypes']['primCountsByType']['Points'],
|
|
|
|
1, "Unexpected number of prototype point clouds")
|
|
|
|
|
|
|
|
# Get the prototypes root.
|
|
|
|
protos_root_path = Sdf.Path("/root/prototypes")
|
|
|
|
prim = stage.GetPrimAtPath(protos_root_path)
|
|
|
|
assert prim
|
|
|
|
self.assertTrue(prim.IsAbstract())
|
|
|
|
|
|
|
|
# Get the first plane instance.
|
|
|
|
prim = stage.GetPrimAtPath("/root/plane_001/Plane_0")
|
|
|
|
assert prim
|
|
|
|
assert prim.IsInstance()
|
|
|
|
|
|
|
|
# Get the second plane instance.
|
|
|
|
prim = stage.GetPrimAtPath("/root/plane/Plane_0")
|
|
|
|
assert prim
|
|
|
|
assert prim.IsInstance()
|
|
|
|
|
|
|
|
# Ensure all the prototype paths are under the pototypes root.
|
|
|
|
for prim in stage.Traverse():
|
|
|
|
if prim.IsInstance():
|
|
|
|
arcs = Usd.PrimCompositionQuery.GetDirectReferences(prim).GetCompositionArcs()
|
|
|
|
for arc in arcs:
|
|
|
|
target_path = arc.GetTargetPrimPath()
|
|
|
|
self.assertTrue(target_path.HasPrefix(protos_root_path))
|
|
|
|
|
|
|
|
def test_export_native_instancing_false(self):
|
|
|
|
"""Test exporting instanced objects with instancing disabled."""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "nested_instancing_test.blend"))
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_export_nested_instancing_false.usda"
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=str(export_path),
|
|
|
|
use_instancing=False
|
|
|
|
)
|
|
|
|
|
|
|
|
# The USD should contain no instances.
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
stats = UsdUtils.ComputeUsdStageStats(stage)
|
|
|
|
self.assertEqual(stats['totalInstanceCount'], 0, "Unexpected number of instances")
|
|
|
|
self.assertEqual(stats['prototypeCount'], 0, "Unexpected number of prototypes")
|
|
|
|
self.assertEqual(stats['primary']['primCountsByType']['Mesh'], 2, "Unexpected number of primary meshes")
|
|
|
|
self.assertEqual(stats['primary']['primCountsByType']['Points'], 4, "Unexpected number of primary point clouds")
|
|
|
|
|
2024-12-13 16:36:22 +01:00
|
|
|
def test_texture_export_hook(self):
|
|
|
|
"""Exporting textures from on_material_export USD hook."""
|
|
|
|
|
|
|
|
# Clear USD hook results.
|
|
|
|
ExportTextureUSDHook.exported_textures = {}
|
|
|
|
|
|
|
|
bpy.utils.register_class(ExportTextureUSDHook)
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_materials_export.usda"
|
|
|
|
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=str(export_path),
|
|
|
|
export_materials=True,
|
|
|
|
generate_preview_surface=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Verify that the exported texture paths were returned as expected.
|
|
|
|
expected = {'/root/_materials/Transforms': './textures/test_grid_<UDIM>.png',
|
|
|
|
'/root/_materials/Clip_With_Round': './textures/test_grid_<UDIM>.png',
|
|
|
|
'/root/_materials/NormalMap': './textures/test_normal.exr',
|
|
|
|
'/root/_materials/Material': './textures/test_grid_<UDIM>.png',
|
|
|
|
'/root/_materials/Clip_With_LessThanInvert': './textures/test_grid_<UDIM>.png',
|
|
|
|
'/root/_materials/NormalMap_Scale_Bias': './textures/test_normal_invertY.exr'}
|
|
|
|
|
|
|
|
self.assertDictEqual(ExportTextureUSDHook.exported_textures,
|
|
|
|
expected,
|
|
|
|
"Unexpected texture export paths")
|
|
|
|
|
|
|
|
bpy.utils.unregister_class(ExportTextureUSDHook)
|
|
|
|
|
|
|
|
# Verify that the texture files were copied as expected.
|
|
|
|
tex_names = ['test_grid_1001.png', 'test_grid_1002.png',
|
|
|
|
'test_normal.exr', 'test_normal_invertY.exr']
|
|
|
|
|
|
|
|
for name in tex_names:
|
|
|
|
tex_path = self.tempdir / "textures" / name
|
|
|
|
self.assertTrue(tex_path.exists(),
|
|
|
|
f"Exported texture {tex_path} doesn't exist")
|
|
|
|
|
|
|
|
def test_inmem_pack_texture_export_hook(self):
|
|
|
|
"""Exporting packed and in memory textures from on_material_export USD hook."""
|
|
|
|
|
|
|
|
# Clear hook results.
|
|
|
|
ExportTextureUSDHook.exported_textures = {}
|
|
|
|
|
|
|
|
bpy.utils.register_class(ExportTextureUSDHook)
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_inmem_pack.blend"))
|
|
|
|
|
|
|
|
export_path = self.tempdir / "usd_materials_inmem_pack.usda"
|
|
|
|
|
|
|
|
self.export_and_validate(
|
|
|
|
filepath=str(export_path),
|
|
|
|
export_materials=True,
|
|
|
|
generate_preview_surface=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Verify that the exported texture paths were returned as expected.
|
|
|
|
expected = {'/root/_materials/MAT_pack_udim': './textures/test_grid_<UDIM>.png',
|
|
|
|
'/root/_materials/MAT_pack_single': './textures/test_single.png',
|
|
|
|
'/root/_materials/MAT_inmem_udim': './textures/inmem_udim.<UDIM>.png',
|
|
|
|
'/root/_materials/MAT_inmem_single': './textures/inmem_single.png'}
|
|
|
|
|
|
|
|
self.assertDictEqual(ExportTextureUSDHook.exported_textures,
|
|
|
|
expected,
|
|
|
|
"Unexpected texture export paths")
|
|
|
|
|
|
|
|
bpy.utils.unregister_class(ExportTextureUSDHook)
|
|
|
|
|
|
|
|
# Verify that the texture files were copied as expected.
|
|
|
|
tex_names = ['test_grid_1001.png', 'test_grid_1002.png',
|
|
|
|
'test_single.png',
|
|
|
|
'inmem_udim.1001.png', 'inmem_udim.1002.png',
|
|
|
|
'inmem_single.png']
|
|
|
|
|
|
|
|
for name in tex_names:
|
|
|
|
tex_path = self.tempdir / "textures" / name
|
|
|
|
self.assertTrue(tex_path.exists(),
|
|
|
|
f"Exported texture {tex_path} doesn't exist")
|
|
|
|
|
2025-03-21 21:29:08 +01:00
|
|
|
def test_naming_collision_hierarchy(self):
|
|
|
|
"""Validate that naming collisions during export are handled correctly"""
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_hierarchy_collision.blend"))
|
|
|
|
export_path = self.tempdir / "usd_hierarchy_collision.usda"
|
|
|
|
self.export_and_validate(filepath=str(export_path))
|
|
|
|
|
|
|
|
expected = (
|
|
|
|
('/root', 'Xform'),
|
|
|
|
('/root/Empty', 'Xform'),
|
|
|
|
('/root/Empty/Par_002', 'Xform'),
|
|
|
|
('/root/Empty/Par_002/Par_1', 'Mesh'),
|
|
|
|
('/root/Empty/Par_003', 'Xform'),
|
|
|
|
('/root/Empty/Par_003/Par_1', 'Mesh'),
|
|
|
|
('/root/Empty/Par_004', 'Xform'),
|
|
|
|
('/root/Empty/Par_004/Par_002', 'Mesh'),
|
|
|
|
('/root/Empty/Par_1', 'Xform'),
|
|
|
|
('/root/Empty/Par_1/Par_1', 'Mesh'),
|
|
|
|
('/root/Level1', 'Xform'),
|
|
|
|
('/root/Level1/Level2', 'Xform'),
|
|
|
|
('/root/Level1/Level2/Par2_002', 'Xform'),
|
|
|
|
('/root/Level1/Level2/Par2_002/Par2_002', 'Mesh'),
|
|
|
|
('/root/Level1/Level2/Par2_1', 'Xform'),
|
|
|
|
('/root/Level1/Level2/Par2_1/Par2_1', 'Mesh'),
|
|
|
|
('/root/Level1/Par2_002', 'Xform'),
|
|
|
|
('/root/Level1/Par2_002/Par2_1', 'Mesh'),
|
|
|
|
('/root/Level1/Par2_1', 'Xform'),
|
|
|
|
('/root/Level1/Par2_1/Par2_1', 'Mesh'),
|
|
|
|
('/root/Test_002', 'Xform'),
|
|
|
|
('/root/Test_002/Test_1', 'Mesh'),
|
|
|
|
('/root/Test_003', 'Xform'),
|
|
|
|
('/root/Test_003/Test_1', 'Mesh'),
|
|
|
|
('/root/Test_004', 'Xform'),
|
|
|
|
('/root/Test_004/Test_002', 'Mesh'),
|
|
|
|
('/root/Test_1', 'Xform'),
|
|
|
|
('/root/Test_1/Test_1', 'Mesh'),
|
|
|
|
('/root/env_light', 'DomeLight'),
|
|
|
|
('/root/xSource_002', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_002', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_002/Dup_002', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_002_0', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_002_0/Dup_002', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_002_1', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_002_1/Dup_002', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_002_2', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_002_2/Dup_002', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_002_3', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_002_3/Dup_002', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_1', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_1/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_1_0', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_1_0/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_1_1', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_1_1/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_1_2', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_1_2/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_002/Dup_1_3', 'Xform'),
|
|
|
|
('/root/xSource_002/Dup_1_3/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_002/xSource_1', 'Mesh'),
|
|
|
|
('/root/xSource_1', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_002', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_002/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_0', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_0/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_001', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_001/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_002', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_002/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_003', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_003/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_004', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_004/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_1', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_1/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_2', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_2/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/Dup_1_3', 'Xform'),
|
|
|
|
('/root/xSource_1/Dup_1_3/Dup_1', 'Mesh'),
|
|
|
|
('/root/xSource_1/xSource_1', 'Mesh')
|
|
|
|
)
|
|
|
|
expected = tuple(sorted(expected, key=lambda pair: pair[0]))
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
actual = ((str(p.GetPath()), p.GetTypeName()) for p in stage.Traverse())
|
|
|
|
actual = tuple(sorted(actual, key=lambda pair: pair[0]))
|
|
|
|
|
|
|
|
self.assertTupleEqual(expected, actual)
|
|
|
|
|
2024-08-27 05:38:55 +02:00
|
|
|
|
2025-02-04 14:51:17 +11:00
|
|
|
class USDHookBase:
|
2024-08-27 05:38:55 +02:00
|
|
|
instructions = {}
|
|
|
|
responses = {}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def follow_instructions(name, operation):
|
|
|
|
instruction = USDHookBase.instructions[operation].pop(0)
|
|
|
|
if instruction == "throw":
|
|
|
|
USDHookBase.responses[operation].append("threw exception")
|
|
|
|
raise RuntimeError(f"** {name} failing {operation} **")
|
|
|
|
elif instruction == "return False":
|
|
|
|
USDHookBase.responses[operation].append("returned False")
|
|
|
|
return False
|
|
|
|
|
|
|
|
USDHookBase.responses[operation].append("returned True")
|
|
|
|
return True
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def do_on_export(name, export_context):
|
|
|
|
stage = export_context.get_stage()
|
|
|
|
depsgraph = export_context.get_depsgraph()
|
|
|
|
if not stage.GetDefaultPrim().IsValid():
|
|
|
|
raise RuntimeError("Unexpected failure: bad stage")
|
|
|
|
if len(depsgraph.ids) == 0:
|
|
|
|
raise RuntimeError("Unexpected failure: bad depsgraph")
|
|
|
|
|
|
|
|
return USDHookBase.follow_instructions(name, "on_export")
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def do_on_material_export(name, export_context, bl_material, usd_material):
|
|
|
|
stage = export_context.get_stage()
|
|
|
|
if stage.expired:
|
|
|
|
raise RuntimeError("Unexpected failure: bad stage")
|
|
|
|
if not usd_material.GetPrim().IsValid():
|
|
|
|
raise RuntimeError("Unexpected failure: bad usd_material")
|
|
|
|
if bl_material is None:
|
|
|
|
raise RuntimeError("Unexpected failure: bad bl_material")
|
|
|
|
|
|
|
|
return USDHookBase.follow_instructions(name, "on_material_export")
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def do_on_import(name, import_context):
|
|
|
|
stage = import_context.get_stage()
|
|
|
|
if not stage.GetDefaultPrim().IsValid():
|
|
|
|
raise RuntimeError("Unexpected failure: bad stage")
|
|
|
|
|
|
|
|
return USDHookBase.follow_instructions(name, "on_import")
|
|
|
|
|
|
|
|
|
|
|
|
class USDHook1(USDHookBase, bpy.types.USDHook):
|
|
|
|
bl_idname = "usd_hook_1"
|
|
|
|
bl_label = "Hook 1"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_export(export_context):
|
|
|
|
return USDHookBase.do_on_export(USDHook1.bl_label, export_context)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_material_export(export_context, bl_material, usd_material):
|
|
|
|
return USDHookBase.do_on_material_export(USDHook1.bl_label, export_context, bl_material, usd_material)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_import(import_context):
|
|
|
|
return USDHookBase.do_on_import(USDHook1.bl_label, import_context)
|
|
|
|
|
|
|
|
|
|
|
|
class USDHook2(USDHookBase, bpy.types.USDHook):
|
|
|
|
bl_idname = "usd_hook_2"
|
|
|
|
bl_label = "Hook 2"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_export(export_context):
|
|
|
|
return USDHookBase.do_on_export(USDHook2.bl_label, export_context)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_material_export(export_context, bl_material, usd_material):
|
|
|
|
return USDHookBase.do_on_material_export(USDHook2.bl_label, export_context, bl_material, usd_material)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_import(import_context):
|
|
|
|
return USDHookBase.do_on_import(USDHook2.bl_label, import_context)
|
|
|
|
|
2023-05-02 08:41:10 +10:00
|
|
|
|
2024-12-13 16:36:22 +01:00
|
|
|
class ExportTextureUSDHook(bpy.types.USDHook):
|
|
|
|
bl_idname = "export_texture_usd_hook"
|
|
|
|
bl_label = "Export Texture USD Hook"
|
|
|
|
|
|
|
|
exported_textures = {}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def on_material_export(export_context, bl_material, usd_material):
|
|
|
|
"""
|
|
|
|
If a texture image node exists in the given material's
|
|
|
|
node tree, call exprt_texture() on the image and cache
|
|
|
|
the returned path.
|
|
|
|
"""
|
|
|
|
tex_image_node = None
|
|
|
|
if bl_material and bl_material.node_tree:
|
|
|
|
for node in bl_material.node_tree.nodes:
|
|
|
|
if node.type == 'TEX_IMAGE':
|
|
|
|
tex_image_node = node
|
|
|
|
|
|
|
|
if tex_image_node is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
tex_path = export_context.export_texture(tex_image_node.image)
|
|
|
|
|
|
|
|
ExportTextureUSDHook.exported_textures[usd_material.GetPath()
|
|
|
|
.pathString] = tex_path
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2023-02-14 12:11:53 +01:00
|
|
|
def main():
|
|
|
|
global args
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
if "--" in sys.argv:
|
|
|
|
argv = [sys.argv[0]] + sys.argv[sys.argv.index("--") + 1:]
|
|
|
|
else:
|
|
|
|
argv = sys.argv
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("--testdir", required=True, type=pathlib.Path)
|
|
|
|
args, remaining = parser.parse_known_args(argv)
|
|
|
|
|
2025-01-26 04:43:16 +01:00
|
|
|
unittest.main(argv=remaining, verbosity=0)
|
2023-02-14 12:11:53 +01:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|