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
|
|
|
|
2023-02-14 12:11:53 +01:00
|
|
|
import pathlib
|
|
|
|
|
import pprint
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
|
|
|
|
from pxr import Usd
|
|
|
|
|
from pxr import UsdUtils
|
|
|
|
|
from pxr import UsdGeom
|
2023-05-01 17:28:23 +02:00
|
|
|
from pxr import UsdShade
|
2024-06-20 03:50:26 +02:00
|
|
|
from pxr import UsdSkel
|
2023-02-14 12:11:53 +01:00
|
|
|
from pxr import Gf
|
|
|
|
|
|
|
|
|
|
import bpy
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
return cls
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
self.assertTrue(
|
|
|
|
|
self.testdir.exists(), "Test dir {0} should exist".format(self.testdir)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
self._tempdir.cleanup()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class USDExportTest(AbstractUSDTest):
|
|
|
|
|
def test_export_usdchecker(self):
|
|
|
|
|
"""Test exporting a scene and verifying it passes the usdchecker test suite"""
|
|
|
|
|
bpy.ops.wm.open_mainfile(
|
|
|
|
|
filepath=str(self.testdir / "usd_materials_export.blend")
|
|
|
|
|
)
|
|
|
|
|
export_path = self.tempdir / "usdchecker.usda"
|
|
|
|
|
res = bpy.ops.wm.usd_export(
|
|
|
|
|
filepath=str(export_path),
|
|
|
|
|
export_materials=True,
|
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
checker = UsdUtils.ComplianceChecker(
|
|
|
|
|
arkit=False,
|
|
|
|
|
skipARKitRootLayerCheck=False,
|
|
|
|
|
rootPackageOnly=False,
|
|
|
|
|
skipVariants=False,
|
|
|
|
|
verbose=False,
|
|
|
|
|
)
|
|
|
|
|
checker.CheckCompliance(str(export_path))
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
res = bpy.ops.wm.usd_export(
|
|
|
|
|
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
|
|
|
)
|
|
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
)
|
|
|
|
|
|
2023-05-01 17:28:23 +02:00
|
|
|
def test_opacity_threshold(self):
|
|
|
|
|
# Note that the scene file used here is shared with a different test.
|
|
|
|
|
# Here we assume that it has a Principled BSDF material with
|
|
|
|
|
# a texture connected to its Base Color input.
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend"))
|
|
|
|
|
|
|
|
|
|
export_path = self.tempdir / "opaque_material.usda"
|
|
|
|
|
res = bpy.ops.wm.usd_export(
|
|
|
|
|
filepath=str(export_path),
|
|
|
|
|
export_materials=True,
|
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
# Inspect and validate the exported USD for the opaque blend case.
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
2023-10-20 10:53:51 -04:00
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Material/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-05-25 23:30:13 +02:00
|
|
|
# Inspect and validate the exported USD for the alpha clip w/Round node case.
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Clip_With_Round/Principled_BSDF")
|
2023-05-01 17:28:23 +02:00
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
2024-05-25 23:30:13 +02:00
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
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-05-25 23:30:13 +02:00
|
|
|
# Inspect and validate the exported USD for the alpha clip w/LessThan+Invert node case.
|
|
|
|
|
shader_prim = stage.GetPrimAtPath("/root/_materials/Clip_With_LessThanInvert/Principled_BSDF")
|
2023-05-01 17:28:23 +02:00
|
|
|
shader = UsdShade.Shader(shader_prim)
|
|
|
|
|
opacity_input = shader.GetInput('opacity')
|
2024-05-25 23:30:13 +02:00
|
|
|
opacity_thresh_input = shader.GetInput('opacityThreshold')
|
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-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-05-02 03:57:25 +02:00
|
|
|
res = bpy.ops.wm.usd_export(filepath=str(export_path), evaluation_mode="RENDER")
|
2024-04-28 03:53:20 +02:00
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
self.check_primvar_missing(prim, "f_color")
|
|
|
|
|
self.check_primvar_missing(prim, "f_byte_color")
|
|
|
|
|
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)
|
|
|
|
|
self.check_primvar_missing(prim, "p_color")
|
|
|
|
|
self.check_primvar_missing(prim, "p_byte_color")
|
|
|
|
|
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)
|
|
|
|
|
self.check_primvar_missing(prim, "sp_color")
|
|
|
|
|
self.check_primvar_missing(prim, "sp_byte_color")
|
|
|
|
|
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)
|
|
|
|
|
self.check_primvar_missing(prim, "p_color")
|
|
|
|
|
self.check_primvar_missing(prim, "p_byte_color")
|
|
|
|
|
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)
|
|
|
|
|
self.check_primvar_missing(prim, "sp_color")
|
|
|
|
|
self.check_primvar_missing(prim, "sp_byte_color")
|
|
|
|
|
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"
|
|
|
|
|
res = bpy.ops.wm.usd_export(filepath=str(export_path), export_animation=True, evaluation_mode="RENDER")
|
|
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# 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"))
|
|
|
|
|
|
|
|
|
|
sparse_frames = [4.0, 5.0, 8.0, 9.0, 12.0, 13.0]
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
2024-06-20 03:50:26 +02:00
|
|
|
def test_animation(self):
|
|
|
|
|
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_anim_test.blend"))
|
|
|
|
|
export_path = self.tempdir / "usd_anim_test.usda"
|
|
|
|
|
res = bpy.ops.wm.usd_export(
|
|
|
|
|
filepath=str(export_path),
|
|
|
|
|
export_animation=True,
|
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
|
|
|
|
|
# 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())
|
|
|
|
|
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()
|
|
|
|
|
self.assertEqual(loc_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
|
|
|
|
|
self.assertEqual(rot_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
|
|
|
|
|
self.assertEqual(scale_samples, [1.0, 2.0, 3.0, 4.0, 5.0])
|
|
|
|
|
|
|
|
|
|
# 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])
|
|
|
|
|
|
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"""
|
|
|
|
|
bpy.ops.wm.open_mainfile(
|
|
|
|
|
filepath=str(self.testdir / "usd_materials_export.blend")
|
|
|
|
|
)
|
|
|
|
|
export_path = self.tempdir / "materialx.usda"
|
|
|
|
|
res = bpy.ops.wm.usd_export(
|
|
|
|
|
filepath=str(export_path),
|
|
|
|
|
export_materials=True,
|
|
|
|
|
generate_materialx_network=True,
|
|
|
|
|
evaluation_mode="RENDER",
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}")
|
|
|
|
|
|
|
|
|
|
stage = Usd.Stage.Open(str(export_path))
|
|
|
|
|
material_prim = stage.GetPrimAtPath("/root/_materials/Material")
|
|
|
|
|
self.assertTrue(material_prim, "Could not find Material prim")
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
self.assertEqual(shader_id, "ND_standard_surface_surfaceshader", "Shader is not a Standard Surface")
|
|
|
|
|
|
2023-05-02 08:41:10 +10:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
unittest.main(argv=remaining)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|