From b265161aeaa4ce79b9950a0be217a9baf71e2dd0 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Mon, 26 Aug 2024 03:26:32 +0200 Subject: [PATCH] USD: Add more thorough testing of various material setups Newly validates the following: - Image mapping transforms on import and export - Typical normal map setups on import and export - Alpha-clip node setups on import (was already tested for export) Pull Request: https://projects.blender.org/blender/blender/pulls/126776 --- .../blender/io/usd/tests/usd_export_test.cc | 8 ++- .../io/usd/tests/usd_usdz_export_test.cc | 1 + tests/data | 2 +- tests/python/bl_usd_export_test.py | 72 ++++++++++++++++--- tests/python/bl_usd_import_test.py | 60 +++++++++++++++- 5 files changed, 127 insertions(+), 16 deletions(-) diff --git a/source/blender/io/usd/tests/usd_export_test.cc b/source/blender/io/usd/tests/usd_export_test.cc index d6309e7cdb8..b515557a3b0 100644 --- a/source/blender/io/usd/tests/usd_export_test.cc +++ b/source/blender/io/usd/tests/usd_export_test.cc @@ -264,9 +264,9 @@ TEST_F(UsdExportTest, usd_export_material) } /* File sanity checks. */ - EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 3); - /* There are 4 materials because of the Dots Stroke. */ - EXPECT_EQ(BLI_listbase_count(&bfile->main->materials), 4); + EXPECT_EQ(BLI_listbase_count(&bfile->main->objects), 6); + /* There is 1 additional material because of the "Dots Stroke". */ + EXPECT_EQ(BLI_listbase_count(&bfile->main->materials), 7); Material *material = reinterpret_cast( BKE_libblock_find_name(bfile->main, ID_MA, "Material")); @@ -279,6 +279,8 @@ TEST_F(UsdExportTest, usd_export_material) params.export_textures = false; params.export_uvmaps = true; params.generate_preview_surface = true; + params.generate_materialx_network = false; + params.convert_world_material = false; params.relative_paths = false; const bool result = USD_export(context, output_filename.c_str(), ¶ms, false, nullptr); diff --git a/source/blender/io/usd/tests/usd_usdz_export_test.cc b/source/blender/io/usd/tests/usd_usdz_export_test.cc index a03ff00e75c..b0157eb53bc 100644 --- a/source/blender/io/usd/tests/usd_usdz_export_test.cc +++ b/source/blender/io/usd/tests/usd_usdz_export_test.cc @@ -96,6 +96,7 @@ TEST_F(UsdUsdzExportTest, usdz_export) USDExportParams params; params.export_materials = false; + params.convert_world_material = false; params.visible_objects_only = false; bool result = USD_export(context, output_filepath, ¶ms, false, nullptr); diff --git a/tests/data b/tests/data index 528aadad4bd..a77ff593427 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit 528aadad4bdbc43b858a1bbc3585ed7c84797106 +Subproject commit a77ff593427db8297faca2ee14ec6bb70e369963 diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 145e73ecd0d..5bc6c3c1ff5 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -132,18 +132,68 @@ class USDExportTest(AbstractUSDTest): Gf.Vec3d(extent[1]), Gf.Vec3d(0.7515701, 0.5500924, 0.9027928) ) - 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")) + def test_material_transforms(self): + """Validate correct export of image mapping parameters to the UsdTransform2d shader def""" - export_path = self.tempdir / "opaque_material.usda" - res = bpy.ops.wm.usd_export( - filepath=str(export_path), - export_materials=True, - evaluation_mode="RENDER", - ) + # Use the common materials .blend file + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend")) + export_path = self.tempdir / "material_transforms.usda" + res = bpy.ops.wm.usd_export(filepath=str(export_path), export_materials=True) + self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}") + + # 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" + res = bpy.ops.wm.usd_export(filepath=str(export_path), export_materials=True) + self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}") + + # 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 + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend")) + export_path = self.tempdir / "material_opacities.usda" + res = bpy.ops.wm.usd_export(filepath=str(export_path), export_materials=True) self.assertEqual({'FINISHED'}, res, f"Unable to export to {export_path}") # Inspect and validate the exported USD for the opaque blend case. diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 778605df355..efef8498fc6 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -33,7 +33,6 @@ class AbstractUSDTest(unittest.TestCase): class USDImportTest(AbstractUSDTest): - def test_import_operator(self): """Test running the import operator on valid and invalid files.""" @@ -203,6 +202,65 @@ class USDImportTest(AbstractUSDTest): self.assertAlmostEqual(2.281, test_cam.shift_x, 3) self.assertAlmostEqual(0.496, test_cam.shift_y, 3) + def test_import_materials(self): + """Validate UsdPreviewSurface shader graphs.""" + + # Use the existing materials test file to create the USD file + # for import. It is validated as part of the bl_usd_export test. + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend")) + testfile = str(self.tempdir / "temp_materials.usda") + res = bpy.ops.wm.usd_export(filepath=str(testfile), export_materials=True) + self.assertEqual({'FINISHED'}, res, f"Unable to export to {testfile}") + + # Reload the empty file and import back in + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "empty.blend")) + res = bpy.ops.wm.usd_import(filepath=testfile) + self.assertEqual({'FINISHED'}, res, f"Unable to import USD file {testfile}") + + # Most shader graph validation should occur through the Hydra render test suite. Here we + # will only check some high-level criteria for each expected node graph. + + def assert_all_nodes_present(mat, node_list): + nodes = mat.node_tree.nodes + self.assertEqual(len(nodes), len(node_list)) + for node in node_list: + self.assertTrue(nodes.find(node) >= 0, f"Could not find node '{node}' in material '{mat.name}'") + + def round_vector(vector): + return [round(c, 5) + 0 for c in vector] + + mat = bpy.data.materials["Material"] + assert_all_nodes_present(mat, ["Principled BSDF", "Image Texture", "UV Map", "Material Output"]) + + mat = bpy.data.materials["Clip_With_LessThanInvert"] + assert_all_nodes_present( + mat, ["Principled BSDF", "Image Texture", "UV Map", "Math", "Math.001", "Material Output"]) + node = [n for n in mat.node_tree.nodes if n.type == 'MATH' and n.operation == "LESS_THAN"][0] + self.assertAlmostEqual(node.inputs[1].default_value, 0.2, 3) + + mat = bpy.data.materials["Clip_With_Round"] + assert_all_nodes_present( + mat, ["Principled BSDF", "Image Texture", "UV Map", "Math", "Math.001", "Material Output"]) + node = [n for n in mat.node_tree.nodes if n.type == 'MATH' and n.operation == "LESS_THAN"][0] + self.assertAlmostEqual(node.inputs[1].default_value, 0.5, 3) + + mat = bpy.data.materials["Transforms"] + assert_all_nodes_present(mat, ["Principled BSDF", "Image Texture", "UV Map", "Mapping", "Material Output"]) + node = mat.node_tree.nodes["Mapping"] + self.assertEqual(round_vector(node.inputs[1].default_value), [0.75, 0.75, 0]) + self.assertEqual(round_vector(node.inputs[2].default_value), [0, 0, 3.14159]) + self.assertEqual(round_vector(node.inputs[3].default_value), [0.5, 0.5, 1]) + + mat = bpy.data.materials["NormalMap"] + assert_all_nodes_present(mat, ["Principled BSDF", "Image Texture", "UV Map", "Normal Map", "Material Output"]) + + mat = bpy.data.materials["NormalMap_Scale_Bias"] + assert_all_nodes_present(mat, ["Principled BSDF", "Image Texture", "UV Map", + "Normal Map", "Vector Math", "Vector Math.001", "Material Output"]) + node = mat.node_tree.nodes["Vector Math"] + self.assertEqual(round_vector(node.inputs[1].default_value), [2, -2, 2]) + self.assertEqual(round_vector(node.inputs[2].default_value), [-1, 1, -1]) + def test_import_shader_varname_with_connection(self): """Test importing USD shader where uv primvar is a connection"""