From 9daded5d87d1e7e89b24b59bf53f6471c2d4a9d3 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Sat, 25 May 2024 23:30:13 +0200 Subject: [PATCH] USD: Use nodes for alpha-clip behavior instead of material properties EEVEE-next has removed the MA_BM_CLIP / alpha_threshold material properties in favor of using nodes for equivalent functionality. This changes USD to build and traverse node graphs during import and export accordingly. Indirectly this allows Cycles to correctly render such materials now too. A complicating factor is that the UsdPreviewSurface defines its opacity threshold using greater-than-equals[1], which Blender does not support (and for which was technically already incorrect as EEVEE-legacy only used greater-than for its shaders). Due to this we actually need to use 2 nodes: A less-than, followed by a one-minus invert, to arrive at the proper value. We'll translate UsdPreviewSurface to this form on Import. For Export we will look for either this 2-node pattern or a Round node plugged into Alpha. Looking for Round is a result of the glTF documentation which recommended the use of this node for thresholds of 0.5[2]. It's a tiny addition that seems reasonable to accommodate. [1] https://openusd.org/release/spec_usdpreviewsurface.html (search for "opacityThreshold") [2] https://docs.blender.org/manual/en/4.2/addons/import_export/scene_gltf2.html#alpha-modes See PR for example images Pull Request: https://projects.blender.org/blender/blender/pulls/122025 --- .../io/usd/intern/usd_reader_material.cc | 111 +++++++++++++----- .../io/usd/intern/usd_reader_material.hh | 17 ++- .../io/usd/intern/usd_writer_material.cc | 42 ++++++- tests/python/bl_usd_export_test.py | 48 ++------ 4 files changed, 138 insertions(+), 80 deletions(-) diff --git a/source/blender/io/usd/intern/usd_reader_material.cc b/source/blender/io/usd/intern/usd_reader_material.cc index 8bf08b5ddae..60d550df917 100644 --- a/source/blender/io/usd/intern/usd_reader_material.cc +++ b/source/blender/io/usd/intern/usd_reader_material.cc @@ -546,17 +546,9 @@ void USDMaterialReader::import_usd_preview(Material *mtl, BKE_ntree_update_main_tree(bmain_, ntree, nullptr); /* Optionally, set the material blend mode. */ - if (params_.set_material_blend) { if (needs_blend(usd_shader)) { - float opacity_threshold = get_opacity_threshold(usd_shader, 0.0f); - if (opacity_threshold > 0.0f) { - mtl->blend_method = MA_BM_CLIP; - mtl->alpha_threshold = opacity_threshold; - } - else { - mtl->blend_method = MA_BM_BLEND; - } + mtl->surface_render_method = MA_SURFACE_METHOD_FORWARD; } } } @@ -576,13 +568,17 @@ void USDMaterialReader::set_principled_node_inputs(bNode *principled, /* Recursively set the principled shader inputs. */ if (pxr::UsdShadeInput diffuse_input = usd_shader.GetInput(usdtokens::diffuseColor)) { - set_node_input(diffuse_input, principled, "Base Color", ntree, column, &context, true); + ExtraLinkInfo extra; + extra.is_color_corrected = true; + set_node_input(diffuse_input, principled, "Base Color", ntree, column, &context, extra); } float emission_strength = 0.0f; if (pxr::UsdShadeInput emissive_input = usd_shader.GetInput(usdtokens::emissiveColor)) { + ExtraLinkInfo extra; + extra.is_color_corrected = true; if (set_node_input( - emissive_input, principled, "Emission Color", ntree, column, &context, true)) + emissive_input, principled, "Emission Color", ntree, column, &context, extra)) { emission_strength = 1.0f; } @@ -593,37 +589,38 @@ void USDMaterialReader::set_principled_node_inputs(bNode *principled, ((bNodeSocketValueFloat *)emission_strength_sock->default_value)->value = emission_strength; if (pxr::UsdShadeInput specular_input = usd_shader.GetInput(usdtokens::specularColor)) { - set_node_input(specular_input, principled, "Specular Tint", ntree, column, &context, false); + set_node_input(specular_input, principled, "Specular Tint", ntree, column, &context); } if (pxr::UsdShadeInput metallic_input = usd_shader.GetInput(usdtokens::metallic)) { - set_node_input(metallic_input, principled, "Metallic", ntree, column, &context, false); + set_node_input(metallic_input, principled, "Metallic", ntree, column, &context); } if (pxr::UsdShadeInput roughness_input = usd_shader.GetInput(usdtokens::roughness)) { - set_node_input(roughness_input, principled, "Roughness", ntree, column, &context, false); + set_node_input(roughness_input, principled, "Roughness", ntree, column, &context); } if (pxr::UsdShadeInput coat_input = usd_shader.GetInput(usdtokens::clearcoat)) { - set_node_input(coat_input, principled, "Coat Weight", ntree, column, &context, false); + set_node_input(coat_input, principled, "Coat Weight", ntree, column, &context); } if (pxr::UsdShadeInput coat_roughness_input = usd_shader.GetInput(usdtokens::clearcoatRoughness)) { - set_node_input( - coat_roughness_input, principled, "Coat Roughness", ntree, column, &context, false); + set_node_input(coat_roughness_input, principled, "Coat Roughness", ntree, column, &context); } if (pxr::UsdShadeInput opacity_input = usd_shader.GetInput(usdtokens::opacity)) { - set_node_input(opacity_input, principled, "Alpha", ntree, column, &context, false); + ExtraLinkInfo extra; + extra.opacity_threshold = get_opacity_threshold(usd_shader, 0.0f); + set_node_input(opacity_input, principled, "Alpha", ntree, column, &context, extra); } if (pxr::UsdShadeInput ior_input = usd_shader.GetInput(usdtokens::ior)) { - set_node_input(ior_input, principled, "IOR", ntree, column, &context, false); + set_node_input(ior_input, principled, "IOR", ntree, column, &context); } if (pxr::UsdShadeInput normal_input = usd_shader.GetInput(usdtokens::normal)) { - set_node_input(normal_input, principled, "Normal", ntree, column, &context, false); + set_node_input(normal_input, principled, "Normal", ntree, column, &context); } } @@ -633,7 +630,7 @@ bool USDMaterialReader::set_node_input(const pxr::UsdShadeInput &usd_input, bNodeTree *ntree, const int column, NodePlacementContext *r_ctx, - bool is_color_corrected) const + const ExtraLinkInfo &extra) const { if (!(usd_input && dest_node && r_ctx)) { return false; @@ -642,8 +639,7 @@ bool USDMaterialReader::set_node_input(const pxr::UsdShadeInput &usd_input, if (usd_input.HasConnectedSource()) { /* The USD shader input has a connected source shader. Follow the connection * and attempt to convert the connected USD shader to a Blender node. */ - return follow_connection( - usd_input, dest_node, dest_socket_name, ntree, column, r_ctx, is_color_corrected); + return follow_connection(usd_input, dest_node, dest_socket_name, ntree, column, r_ctx, extra); } else { /* Set the destination node socket value from the USD shader input value. */ @@ -853,13 +849,53 @@ static IntermediateNode add_separate_color(const pxr::UsdShadeShader &usd_shader return separate_color; } +static IntermediateNode add_lessthan(bNodeTree *ntree, + float threshold, + int column, + NodePlacementContext *r_ctx) +{ + float locx = 0.0f; + float locy = 0.0f; + compute_node_loc(column, &locx, &locy, r_ctx); + + IntermediateNode lessthan{}; + lessthan.node = add_node(nullptr, ntree, SH_NODE_MATH, locx, locy); + lessthan.node->custom1 = NODE_MATH_LESS_THAN; + lessthan.sock_input_name = "Value"; + lessthan.sock_output_name = "Value"; + + bNodeSocket *thresh_sock = blender::bke::nodeFindSocket(lessthan.node, SOCK_IN, "Value_001"); + ((bNodeSocketValueFloat *)thresh_sock->default_value)->value = threshold; + + return lessthan; +} + +static IntermediateNode add_oneminus(bNodeTree *ntree, int column, NodePlacementContext *r_ctx) +{ + float locx = 0.0f; + float locy = 0.0f; + compute_node_loc(column, &locx, &locy, r_ctx); + + /* An "invert" node : 1.0f - Value_001 */ + IntermediateNode oneminus{}; + oneminus.node = add_node(nullptr, ntree, SH_NODE_MATH, locx, locy); + oneminus.node->custom1 = NODE_MATH_SUBTRACT; + oneminus.sock_input_name = "Value_001"; + oneminus.sock_output_name = "Value"; + + bNodeSocket *val_sock = blender::bke::nodeFindSocket(oneminus.node, SOCK_IN, "Value"); + ((bNodeSocketValueFloat *)val_sock->default_value)->value = 1.0f; + + return oneminus; +} + bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input, bNode *dest_node, const char *dest_socket_name, bNodeTree *ntree, int column, NodePlacementContext *r_ctx, - bool is_color_corrected) const + const ExtraLinkInfo &extra) const { if (!(usd_input && dest_node && dest_socket_name && ntree && r_ctx)) { return false; @@ -974,6 +1010,19 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input, target_sock_name = separate_color.sock_input_name; } + /* Handle opacity threshold if necessary. */ + if (source_name == usdtokens::a && extra.opacity_threshold > 0.0f) { + /* USD defines the threshold as >= but Blender does not have that operation. Use < instead + * and then invert it. */ + IntermediateNode lessthan = add_lessthan(ntree, extra.opacity_threshold, column + 1, r_ctx); + IntermediateNode invert = add_oneminus(ntree, column + 1, r_ctx); + link_nodes( + ntree, lessthan.node, lessthan.sock_output_name, invert.node, invert.sock_input_name); + link_nodes(ntree, invert.node, invert.sock_output_name, dest_node, dest_socket_name); + target_node = lessthan.node; + target_sock_name = lessthan.sock_input_name; + } + convert_usd_uv_texture(source_shader, source_name, target_node, @@ -981,7 +1030,7 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input, ntree, column + shift, r_ctx, - is_color_corrected); + extra); } else if (shader_id == usdtokens::UsdPrimvarReader_float2) { convert_usd_primvar_reader_float2( @@ -1001,7 +1050,7 @@ void USDMaterialReader::convert_usd_uv_texture(const pxr::UsdShadeShader &usd_sh bNodeTree *ntree, const int column, NodePlacementContext *r_ctx, - bool is_color_corrected) const + const ExtraLinkInfo &extra) const { if (!usd_shader || !dest_node || !ntree || !dest_socket_name || !bmain_ || !r_ctx) { return; @@ -1026,7 +1075,7 @@ void USDMaterialReader::convert_usd_uv_texture(const pxr::UsdShadeShader &usd_sh cache_node(r_ctx->node_cache, usd_shader, tex_image); /* Load the texture image. */ - load_tex_image(usd_shader, tex_image, is_color_corrected); + load_tex_image(usd_shader, tex_image, extra); } /* Connect to destination node input. */ @@ -1038,7 +1087,7 @@ void USDMaterialReader::convert_usd_uv_texture(const pxr::UsdShadeShader &usd_sh /* Connect the texture image node "Vector" input. */ if (pxr::UsdShadeInput st_input = usd_shader.GetInput(usdtokens::st)) { - set_node_input(st_input, tex_image, "Vector", ntree, column, r_ctx, false); + set_node_input(st_input, tex_image, "Vector", ntree, column, r_ctx); } } @@ -1116,13 +1165,13 @@ void USDMaterialReader::convert_usd_transform_2d(const pxr::UsdShadeShader &usd_ /* Connect the mapping node "Vector" input. */ if (pxr::UsdShadeInput in_input = usd_shader.GetInput(usdtokens::in)) { - set_node_input(in_input, mapping, "Vector", ntree, column, r_ctx, false); + set_node_input(in_input, mapping, "Vector", ntree, column, r_ctx); } } void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader, bNode *tex_image, - bool is_color_corrected) const + const ExtraLinkInfo &extra) const { if (!(usd_shader && tex_image && tex_image->type == SH_NODE_TEX_IMAGE)) { return; @@ -1227,7 +1276,7 @@ void USDMaterialReader::load_tex_image(const pxr::UsdShadeShader &usd_shader, if (color_space == usdtokens::auto_) { /* If it's auto, determine whether to apply color correction based * on incoming connection (passed in from outer functions). */ - STRNCPY(image->colorspace_settings.name, is_color_corrected ? "sRGB" : "Non-Color"); + STRNCPY(image->colorspace_settings.name, extra.is_color_corrected ? "sRGB" : "Non-Color"); } else if (color_space == usdtokens::sRGB) { diff --git a/source/blender/io/usd/intern/usd_reader_material.hh b/source/blender/io/usd/intern/usd_reader_material.hh index 28c6c0282ac..e8572e2aeff 100644 --- a/source/blender/io/usd/intern/usd_reader_material.hh +++ b/source/blender/io/usd/intern/usd_reader_material.hh @@ -53,6 +53,15 @@ struct NodePlacementContext { } }; +/* Helper struct which carries an assortment of optional + * information that is sometimes required when linking + * nodes together. */ +struct ExtraLinkInfo { + bool is_color_corrected = false; + + float opacity_threshold = 0.0f; +}; + /* Converts USD materials to Blender representation. */ /** @@ -110,7 +119,7 @@ class USDMaterialReader { bNodeTree *ntree, int column, NodePlacementContext *r_ctx, - bool is_color_corrected) const; + const ExtraLinkInfo &extra = {}) const; /** * Follow the connected source of the USD input to create corresponding inputs @@ -122,7 +131,7 @@ class USDMaterialReader { bNodeTree *ntree, int column, NodePlacementContext *r_ctx, - bool is_color_corrected = false) const; + const ExtraLinkInfo &extra = {}) const; void convert_usd_uv_texture(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &usd_source_name, @@ -131,7 +140,7 @@ class USDMaterialReader { bNodeTree *ntree, int column, NodePlacementContext *r_ctx, - bool is_color_corrected = false) const; + const ExtraLinkInfo &extra = {}) const; void convert_usd_transform_2d(const pxr::UsdShadeShader &usd_shader, bNode *dest_node, @@ -146,7 +155,7 @@ class USDMaterialReader { */ void load_tex_image(const pxr::UsdShadeShader &usd_shader, bNode *tex_image, - bool is_color_corrected = false) const; + const ExtraLinkInfo &extra = {}) const; /** * This function creates a Blender UV Map node, under the simplifying assumption that diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index d7a6049a950..599ff0113f7 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -298,12 +298,42 @@ static void create_usd_preview_surface_material(const USDExporterContext &usd_ex } /* Set opacityThreshold if an alpha cutout is used. */ - if ((input_spec.input_name == usdtokens::opacity) && - (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)); + if (input_spec.input_name == usdtokens::opacity) { + float threshold = 0.0f; + + /* The immediate upstream node should either be a Math Round or a Math 1-minus. */ + bNodeLink *math_link = traverse_channel(sock, SH_NODE_MATH); + if (math_link && math_link->fromnode) { + bNode *math_node = math_link->fromnode; + + if (math_node->custom1 == NODE_MATH_ROUND) { + threshold = 0.5f; + } + else if (math_node->custom1 == NODE_MATH_SUBTRACT) { + /* If this is the 1-minus node, we need to search upstream to find the less-than. */ + bNodeSocket *sock = blender::bke::nodeFindSocket(math_node, SOCK_IN, "Value"); + if (((bNodeSocketValueFloat *)sock->default_value)->value == 1.0f) { + sock = blender::bke::nodeFindSocket(math_node, SOCK_IN, "Value_001"); + math_link = traverse_channel(sock, SH_NODE_MATH); + if (math_link && math_link->fromnode) { + math_node = math_link->fromnode; + + if (math_node->custom1 == NODE_MATH_LESS_THAN) { + /* We found the upstream less-than with the threshold value. */ + bNodeSocket *threshold_sock = blender::bke::nodeFindSocket( + math_node, SOCK_IN, "Value_001"); + threshold = ((bNodeSocketValueFloat *)threshold_sock->default_value)->value; + } + } + } + } + } + + if (threshold > 0.0f) { + pxr::UsdShadeInput opacity_threshold_input = preview_surface.CreateInput( + usdtokens::opacityThreshold, pxr::SdfValueTypeNames->Float); + opacity_threshold_input.GetAttr().Set(pxr::VtValue(threshold)); + } } } else if (input_spec.set_default_value) { diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 76f6c87caa6..c350d17a42c 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -148,53 +148,23 @@ class USDExportTest(AbstractUSDTest): 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") + self.assertAlmostEqual(opacity_input.Get(), 1.0, 2, "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("/root/_materials/Material/Principled_BSDF") + # 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") shader = UsdShade.Shader(shader_prim) opacity_input = shader.GetInput('opacity') - opacity_thres_input = shader.GetInput('opacityThreshold') + opacity_thresh_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") + self.assertAlmostEqual(opacity_thresh_input.Get(), 0.5, 2, "Opacity threshold input should be 0.5") - # 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("/root/_materials/Material/Principled_BSDF") + # 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") shader = UsdShade.Shader(shader_prim) opacity_input = shader.GetInput('opacity') - opacity_thres_input = shader.GetInput('opacityThreshold') + opacity_thresh_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") + self.assertAlmostEqual(opacity_thresh_input.Get(), 0.2, 2, "Opacity threshold input should be 0.2") def check_primvar(self, prim, pv_name, pv_typeName, pv_interp, elements_len): pv = UsdGeom.PrimvarsAPI(prim).GetPrimvar(pv_name)