From 033703c933a8709b216003d3c3851c0be4517277 Mon Sep 17 00:00:00 2001 From: Matt McLin Date: Mon, 17 Apr 2023 23:46:49 -0700 Subject: [PATCH 1/2] Fix T107062: support opacityThreshold when exporting USD This commit extends the USD Preview Surface material support to author the opacityThreshold attribute of materials on export, when the Alpha Clip blend mode is selected. When authoring alpha cutouts in Blender, one sets the Blend Mode to "Alpha Clip", and the Clip Threshold to some value greater than zero. When this case is detected on export, we now author the opacityThreshold attribute to match the specified clip threshold. Note that opacityThreshold is already handled correctly on import, so this change allows the feature to be fully round-tripped. --- .../io/usd/intern/usd_writer_material.cc | 15 ++++ tests/python/bl_usd_export_test.py | 70 +++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index 6c72b32f62f..8e1e34d685b 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -46,6 +46,7 @@ static const pxr::TfToken primvar_float2("UsdPrimvarReader_float2", pxr::TfToken static const pxr::TfToken roughness("roughness", pxr::TfToken::Immortal); static const pxr::TfToken specular("specular", pxr::TfToken::Immortal); static const pxr::TfToken opacity("opacity", pxr::TfToken::Immortal); +static const pxr::TfToken opacityThreshold("opacityThreshold", pxr::TfToken::Immortal); static const pxr::TfToken surface("surface", pxr::TfToken::Immortal); static const pxr::TfToken perspective("perspective", pxr::TfToken::Immortal); static const pxr::TfToken orthographic("orthographic", pxr::TfToken::Immortal); @@ -143,6 +144,8 @@ void create_usd_preview_surface_material(const USDExporterContext &usd_export_co const InputSpecMap &input_map = preview_surface_input_map(); + bool has_opacity = false; + /* Set the preview surface inputs. */ LISTBASE_FOREACH (bNodeSocket *, sock, &node->inputs) { @@ -166,6 +169,10 @@ void create_usd_preview_surface_material(const USDExporterContext &usd_export_co preview_surface.CreateInput(input_spec.input_name, input_spec.input_type) .ConnectToSource(created_shader.ConnectableAPI(), input_spec.source_name); set_normal_texture_range(created_shader, input_spec); + + if (input_spec.input_name == usdtokens::opacity) { + has_opacity = true; + } } else if (input_spec.set_default_value) { /* Set hardcoded value. */ @@ -202,6 +209,14 @@ void create_usd_preview_surface_material(const USDExporterContext &usd_export_co create_uvmap_shader( usd_export_context, input_node, usd_material, created_shader, default_uv_sampler); } + + /* Set opacityThreshold if an alpha cutout is used. */ + if (has_opacity) { + if ((material->blend_method == MA_BM_CLIP) && (material->alpha_threshold > 0.0)) { + pxr::UsdShadeInput opacity_threshold_input = preview_surface.CreateInput(usdtokens::opacityThreshold, pxr::SdfValueTypeNames->Float); + opacity_threshold_input.GetAttr().Set(pxr::VtValue(material->alpha_threshold)); + } + } } void set_normal_texture_range(pxr::UsdShadeShader &usd_shader, const InputSpec &input_spec) diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 1080acaf7e6..8493867df0e 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -8,6 +8,7 @@ import unittest from pxr import Usd from pxr import UsdUtils from pxr import UsdGeom +from pxr import UsdShade from pxr import Gf import bpy @@ -124,6 +125,75 @@ 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. + print(self.testdir) + 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)) + shader_prim = stage.GetPrimAtPath("/_materials/Material/Principled_BSDF") + shader = UsdShade.Shader(shader_prim) + opacity_input = shader.GetInput('opacity') + self.assertEqual(opacity_input.HasConnectedSource(), False, "Opacity input should not be connected for opaque material") + self.assertAlmostEqual(opacity_input.Get(), 1.0, "Opacity input should be set to 1") + + # The material already has a texture input to the Base Color. + # Now also link this texture to the Alpha input. + # Set an opacity threshold appropriate for alpha clipping. + mat = bpy.data.materials['Material'] + bsdf = mat.node_tree.nodes['Principled BSDF'] + tex_output = bsdf.inputs['Base Color'].links[0].from_node.outputs['Color'] + alpha_input = bsdf.inputs['Alpha'] + mat.node_tree.links.new(tex_output,alpha_input) + bpy.data.materials['Material'].blend_method = 'CLIP' + bpy.data.materials['Material'].alpha_threshold = 0.01 + export_path = self.tempdir / "alphaclip_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 alpha clip case. + stage = Usd.Stage.Open(str(export_path)) + shader_prim = stage.GetPrimAtPath("/_materials/Material/Principled_BSDF") + shader = UsdShade.Shader(shader_prim) + opacity_input = shader.GetInput('opacity') + opacity_thres_input = shader.GetInput('opacityThreshold') + self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected") + self.assertGreater(opacity_thres_input.Get(), 0.0, "Opacity threshold input should be > 0") + + + # Modify material again, this time with alpha blend. + bpy.data.materials['Material'].blend_method = 'BLEND' + export_path = self.tempdir / "alphablend_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 alpha blend case. + stage = Usd.Stage.Open(str(export_path)) + shader_prim = stage.GetPrimAtPath("/_materials/Material/Principled_BSDF") + shader = UsdShade.Shader(shader_prim) + opacity_input = shader.GetInput('opacity') + opacity_thres_input = shader.GetInput('opacityThreshold') + self.assertEqual(opacity_input.HasConnectedSource(), True, "Alpha input should be connected") + self.assertEqual(opacity_thres_input.Get(), None, "Opacity threshold should not be specified for alpha blend") def main(): global args -- 2.30.2 From a31fb92e995777dfa7f6f7a9ab21604e7086bbea Mon Sep 17 00:00:00 2001 From: Matt McLin Date: Sun, 23 Apr 2023 21:56:40 -0700 Subject: [PATCH 2/2] Remove debug print statement --- tests/python/bl_usd_export_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 8493867df0e..9097012cfce 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -129,7 +129,6 @@ class USDExportTest(AbstractUSDTest): # 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. - print(self.testdir) bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "usd_materials_export.blend")) export_path = self.tempdir / "opaque_material.usda" -- 2.30.2