From 46ec2777134ce580977ecfb1036b5f3208f95015 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Mon, 19 May 2025 19:47:22 +0200 Subject: [PATCH] USD: Add support for UsdPrimvarReader_TYPE in materials Add support for the UsdPrimvarReader_TYPE templates for both import and export. These are used by several USD test assets and support here represents the last major piece of the UsdPreviewSurface spec to be implemented. On import these become `Attribute` nodes and on export the `Attribute` nodes will become `UsdPrimvarReader_TYPE`'s accordingly. Import: - `UsdPrimvarReader_float` and `UsdPrimvarReader_int` will use the `Fac` output - `UsdPrimvarReader_float3` and `UsdPrimvarReader_float4` will use the `Color` output - `UsdPrimvarReader_vector`, `UsdPrimvarReader_normal`, and `UsdPrimvarReader_point` will use the `Vector` output Export (only `Geometry` Attribute types are considered): - `Fac` will use `UsdPrimvarReader_float` - `Color` will use `UsdPrimvarReader_float3` - `Vector` will use `UsdPrimvarReader_vector` - `Alpha` is not considered MaterialX note: Hydra-native support is a bit more involved and will have to be done separately. Hydra w/USD sync is trivial to implement but those changes have been left out here. Pull Request: https://projects.blender.org/blender/blender/pulls/135143 --- .../io/usd/intern/usd_reader_material.cc | 79 ++++++++++++++++ .../io/usd/intern/usd_reader_material.hh | 9 +- .../io/usd/intern/usd_writer_material.cc | 89 +++++++++++++++++-- .../files/usd/usd_materials_attributes.blend | 3 + tests/python/bl_usd_export_test.py | 26 ++++++ tests/python/bl_usd_import_test.py | 36 ++++++++ 6 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 tests/files/usd/usd_materials_attributes.blend diff --git a/source/blender/io/usd/intern/usd_reader_material.cc b/source/blender/io/usd/intern/usd_reader_material.cc index 6770b0bd12e..3f51c707517 100644 --- a/source/blender/io/usd/intern/usd_reader_material.cc +++ b/source/blender/io/usd/intern/usd_reader_material.cc @@ -1082,6 +1082,18 @@ bool USDMaterialReader::follow_connection(const pxr::UsdShadeInput &usd_input, else if (shader_id == usdtokens::UsdTransform2d) { convert_usd_transform_2d(source_shader, dest_node, dest_socket_name, ntree, column + 1, ctx); } + else { + /* Handle any remaining "generic" primvar readers. */ + StringRef shader_id_name(shader_id.GetString()); + if (shader_id_name.startswith("UsdPrimvarReader_")) { + int64_t type_offset = shader_id_name.rfind('_'); + if (type_offset >= 0) { + StringRef output_type = shader_id_name.drop_prefix(type_offset + 1); + convert_usd_primvar_reader_generic( + source_shader, output_type, dest_node, dest_socket_name, ntree, column + 1, ctx); + } + } + } return true; } @@ -1418,6 +1430,73 @@ void USDMaterialReader::convert_usd_primvar_reader_float2(const pxr::UsdShadeSha link_nodes(ntree, uv_map, "UV", dest_node, dest_socket_name); } +void USDMaterialReader::convert_usd_primvar_reader_generic(const pxr::UsdShadeShader &usd_shader, + const StringRef output_type, + bNode *dest_node, + const StringRefNull dest_socket_name, + bNodeTree *ntree, + const int column, + NodePlacementContext &ctx) const +{ + if (!usd_shader || !dest_node || !ntree) { + return; + } + + bNode *attribute = ctx.get_cached_node(usd_shader); + + if (attribute == nullptr) { + const float2 loc = ctx.compute_node_loc(column); + + /* Create the attribute node. */ + attribute = add_node(ntree, SH_NODE_ATTRIBUTE, loc); + + /* Cache newly created node. */ + ctx.cache_node(usd_shader, attribute); + + /* Set the attribute name. */ + pxr::UsdShadeInput varname_input = usd_shader.GetInput(usdtokens::varname); + + /* First check if the shader's "varname" input is connected to another source, + * and use that instead if so. */ + if (varname_input) { + for (const pxr::UsdShadeConnectionSourceInfo &source_info : + varname_input.GetConnectedSources()) + { + pxr::UsdShadeShader shader = pxr::UsdShadeShader(source_info.source.GetPrim()); + pxr::UsdShadeInput secondary_varname_input = shader.GetInput(source_info.sourceName); + if (secondary_varname_input) { + varname_input = secondary_varname_input; + break; + } + } + } + + if (varname_input) { + pxr::VtValue varname_val; + /* The varname input may be a string or TfToken, so just cast it to a string. + * The Cast function is defined to provide an empty result if it fails. */ + if (varname_input.Get(&varname_val) && varname_val.CanCastToTypeid(typeid(std::string))) { + std::string varname = varname_val.Cast().Get(); + if (!varname.empty()) { + NodeShaderAttribute *storage = (NodeShaderAttribute *)attribute->storage; + STRNCPY(storage->name, varname.c_str()); + } + } + } + } + + /* Connect to destination node input. */ + if (ELEM(output_type, "float", "int")) { + link_nodes(ntree, attribute, "Fac", dest_node, dest_socket_name); + } + else if (ELEM(output_type, "float3", "float4")) { + link_nodes(ntree, attribute, "Color", dest_node, dest_socket_name); + } + else if (ELEM(output_type, "vector", "normal", "point")) { + link_nodes(ntree, attribute, "Vector", dest_node, dest_socket_name); + } +} + void build_material_map(const Main *bmain, blender::Map &r_mat_map) { BLI_assert_msg(r_mat_map.is_empty(), "The incoming material map should be empty"); diff --git a/source/blender/io/usd/intern/usd_reader_material.hh b/source/blender/io/usd/intern/usd_reader_material.hh index 465c7bc0a04..913d8e00f7b 100644 --- a/source/blender/io/usd/intern/usd_reader_material.hh +++ b/source/blender/io/usd/intern/usd_reader_material.hh @@ -188,8 +188,6 @@ class USDMaterialReader { /** * This function creates a Blender UV Map node, under the simplifying assumption that * UsdPrimvarReader_float2 shaders output UV coordinates. - * TODO(makowalski): investigate supporting conversion to other Blender node types - * (e.g., Attribute Nodes) if needed. */ void convert_usd_primvar_reader_float2(const pxr::UsdShadeShader &usd_shader, const pxr::TfToken &usd_source_name, @@ -198,6 +196,13 @@ class USDMaterialReader { bNodeTree *ntree, int column, NodePlacementContext &ctx) const; + void convert_usd_primvar_reader_generic(const pxr::UsdShadeShader &usd_shader, + StringRef output_type, + bNode *dest_node, + const StringRefNull dest_socket_name, + bNodeTree *ntree, + int column, + NodePlacementContext &ctx) const; }; /* Utility functions. */ diff --git a/source/blender/io/usd/intern/usd_writer_material.cc b/source/blender/io/usd/intern/usd_writer_material.cc index d045f4d1606..9770feab386 100644 --- a/source/blender/io/usd/intern/usd_writer_material.cc +++ b/source/blender/io/usd/intern/usd_writer_material.cc @@ -61,7 +61,10 @@ static const pxr::TfToken preview_shader("previewShader", pxr::TfToken::Immortal static const pxr::TfToken preview_surface("UsdPreviewSurface", pxr::TfToken::Immortal); static const pxr::TfToken UsdTransform2d("UsdTransform2d", pxr::TfToken::Immortal); static const pxr::TfToken uv_texture("UsdUVTexture", pxr::TfToken::Immortal); +static const pxr::TfToken primvar_float("UsdPrimvarReader_float", pxr::TfToken::Immortal); static const pxr::TfToken primvar_float2("UsdPrimvarReader_float2", pxr::TfToken::Immortal); +static const pxr::TfToken primvar_float3("UsdPrimvarReader_float3", pxr::TfToken::Immortal); +static const pxr::TfToken primvar_vector("UsdPrimvarReader_vector", pxr::TfToken::Immortal); 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); @@ -119,6 +122,11 @@ static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &u static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &usd_export_context, const pxr::UsdShadeMaterial &material, bNode *node); +static pxr::UsdShadeShader create_primvar_reader_shader( + const USDExporterContext &usd_export_context, + const pxr::UsdShadeMaterial &material, + const pxr::TfToken &primvar_type, + const bNode *node); static void create_uv_input(const USDExporterContext &usd_export_context, bNodeSocket *input_socket, pxr::UsdShadeMaterial &usd_material, @@ -183,14 +191,13 @@ static void process_inputs(const USDExporterContext &usd_export_context, continue; } + const InputSpec &input_spec = *spec; + /* Allow scaling inputs. */ float input_scale = 1.0; - const InputSpec &input_spec = *spec; - bNodeLink *input_link = traverse_channel(sock, SH_NODE_TEX_IMAGE); - + /* Don't export emission color if strength is zero. */ if (input_spec.input_name == usdtokens::emissive_color) { - /* Don't export emission color if strength is zero. */ const bNodeSocket *emission_strength_sock = bke::node_find_socket( *node, SOCK_IN, "Emission Strength"); if (!emission_strength_sock) { @@ -203,6 +210,10 @@ static void process_inputs(const USDExporterContext &usd_export_context, } } + bool processed = false; + + /* Check for an upstream Image node. */ + const bNodeLink *input_link = traverse_channel(sock, SH_NODE_TEX_IMAGE); if (input_link) { /* Convert the texture image node connected to this input. */ bNode *input_node = input_link->fromnode; @@ -347,10 +358,60 @@ static void process_inputs(const USDExporterContext &usd_export_context, opacity_threshold_input.GetAttr().Set(pxr::VtValue(threshold)); } } - } - else if (input_spec.set_default_value) { - /* Set hardcoded value. */ + processed = true; + } + + if (processed) { + continue; + } + + /* No upstream Image was found. Check for an Attribute node instead */ + input_link = traverse_channel(sock, SH_NODE_ATTRIBUTE); + if (input_link) { + const bNode *attr_node = input_link->fromnode; + const NodeShaderAttribute *storage = (NodeShaderAttribute *)attr_node->storage; + + if (storage->type == SHD_ATTRIBUTE_GEOMETRY) { + pxr::SdfValueTypeName output_type; + pxr::UsdShadeShader usd_shader; + if (STREQ(input_link->fromsock->identifier, "Color")) { + output_type = pxr::SdfValueTypeNames->Float3; + usd_shader = create_primvar_reader_shader( + usd_export_context, usd_material, usdtokens::primvar_float3, attr_node); + } + else if (STREQ(input_link->fromsock->identifier, "Vector")) { + output_type = pxr::SdfValueTypeNames->Float3; + usd_shader = create_primvar_reader_shader( + usd_export_context, usd_material, usdtokens::primvar_vector, attr_node); + } + else if (STREQ(input_link->fromsock->identifier, "Fac")) { + output_type = pxr::SdfValueTypeNames->Float; + usd_shader = create_primvar_reader_shader( + usd_export_context, usd_material, usdtokens::primvar_float, attr_node); + } + + std::string attr_name = make_safe_name(storage->name, + usd_export_context.export_params.allow_unicode); + usd_shader.CreateInput(usdtokens::varname, pxr::SdfValueTypeNames->String).Set(attr_name); + + pxr::UsdShadeConnectionSourceInfo source_info(usd_shader.ConnectableAPI(), + usdtokens::result, + pxr::UsdShadeAttributeType::Output, + output_type); + shader.CreateInput(input_spec.input_name, input_spec.input_type) + .ConnectToSource(source_info); + + processed = true; + } + } + + if (processed) { + continue; + } + + /* No upstream nodes, just set a default constant. */ + if (input_spec.set_default_value) { switch (sock->type) { case SOCK_FLOAT: { create_input( @@ -1085,6 +1146,20 @@ static pxr::UsdShadeShader create_usd_preview_shader(const USDExporterContext &u return shader; } +static pxr::UsdShadeShader create_primvar_reader_shader( + const USDExporterContext &usd_export_context, + const pxr::UsdShadeMaterial &material, + const pxr::TfToken &primvar_type, + const bNode *node) +{ + pxr::SdfPath shader_path = material.GetPath().AppendChild( + pxr::TfToken(make_safe_name(node->name, usd_export_context.export_params.allow_unicode))); + pxr::UsdShadeShader shader = pxr::UsdShadeShader::Define(usd_export_context.stage, shader_path); + + shader.CreateIdAttr(pxr::VtValue(primvar_type)); + return shader; +} + static std::string get_tex_image_asset_filepath(const Image *ima) { char filepath[FILE_MAX]; diff --git a/tests/files/usd/usd_materials_attributes.blend b/tests/files/usd/usd_materials_attributes.blend new file mode 100644 index 00000000000..098506e575d --- /dev/null +++ b/tests/files/usd/usd_materials_attributes.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f56962fb9da5282c58eaf986709a12c86d67769382a1ffe4c3547110ad4b516 +size 110649 diff --git a/tests/python/bl_usd_export_test.py b/tests/python/bl_usd_export_test.py index 21ed8c9b2ec..5b3c5bb7de5 100644 --- a/tests/python/bl_usd_export_test.py +++ b/tests/python/bl_usd_export_test.py @@ -461,6 +461,32 @@ class USDExportTest(AbstractUSDTest): input_displacement = shader_surface.GetInput('displacement') self.assertTrue(input_displacement.Get() is None) + 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") + def test_export_metaballs(self): """Validate correct export of Metaball objects. These are written out as Meshes.""" diff --git a/tests/python/bl_usd_import_test.py b/tests/python/bl_usd_import_test.py index 9a10eb220e0..10660f77969 100644 --- a/tests/python/bl_usd_import_test.py +++ b/tests/python/bl_usd_import_test.py @@ -471,6 +471,42 @@ class USDImportTest(AbstractUSDTest): mat = bpy.data.materials["mid_0_0_scale_0_3"] assert_displacement(mat, None, 0.0, 0.3) + def test_import_material_attributes(self): + """Validate correct import of Attribute information from UsdPrimvarReaders""" + + # 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_attributes.blend")) + testfile = str(self.tempdir / "usd_materials_attributes.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_attribute(mat, attribute_name, from_socket, to_socket): + nodes = [n for n in mat.node_tree.nodes if n.type == 'ATTRIBUTE' and n.attribute_name == attribute_name] + self.assertTrue(len(nodes) == 1) + outputs = [o for o in nodes[0].outputs if o.identifier == from_socket] + self.assertTrue(len(outputs) == 1) + self.assertTrue(len(outputs[0].links) == 1) + link = outputs[0].links[0] + self.assertEqual(link.from_socket.identifier, from_socket) + self.assertEqual(link.to_socket.identifier, to_socket) + + mat = bpy.data.materials["Material"] + self.assert_all_nodes_present( + mat, ["Principled BSDF", "Attribute", "Attribute.001", "Attribute.002", "Material Output"]) + + assert_attribute(mat, "displayColor", "Color", "Base Color") + assert_attribute(mat, "f_vec", "Vector", "Normal") + assert_attribute(mat, "f_float", "Fac", "Roughness") + def test_import_shader_varname_with_connection(self): """Test importing USD shader where uv primvar is a connection"""