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"""