diff --git a/test/sdf/basic_shapes.sdf b/test/sdf/basic_shapes.sdf index 0191bda2e..32e0b416f 100644 --- a/test/sdf/basic_shapes.sdf +++ b/test/sdf/basic_shapes.sdf @@ -113,6 +113,10 @@ 0.1 + + 0 0.1 0.2 + 0.12 0.23 0.34 0.56 + @@ -136,6 +140,18 @@ 1.2 2.3 3.4 + + 0.2 0.5 0.1 1.0 + 0.7 0.3 0.5 0.9 + + + albedo_map.png + normal_map.png + roughness_map.png + metalness_map.png + + + diff --git a/usd/include/sdf/usd/Conversions.hh b/usd/include/sdf/usd/Conversions.hh new file mode 100644 index 000000000..b1409a7af --- /dev/null +++ b/usd/include/sdf/usd/Conversions.hh @@ -0,0 +1,54 @@ +/* + * Copyright 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + + +#ifndef SDF_USD_CONVERSIONS_HH_ +#define SDF_USD_CONVERSIONS_HH_ + +#include + +#include + +#include "sdf/Material.hh" +#include "sdf/sdf_config.h" +#include "sdf/usd/Export.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief Specialized conversion from an Ignition Common Material + /// to a SDF material + /// \param[in] _in Ignition Common Material. + /// \return SDF material. + IGNITION_SDFORMAT_USD_VISIBLE + sdf::Material convert(const ignition::common::Material *_in); + + /// \brief Specialized conversion from an SDF material to a Ignition Common + /// material. + /// \param[in] _in SDF material. + /// \param[out] _out The Ignition Common Material. + IGNITION_SDFORMAT_USD_VISIBLE + void convert(const sdf::Material &_in, ignition::common::Material &_out); + } + } +} + +#endif diff --git a/usd/include/sdf/usd/UsdError.hh b/usd/include/sdf/usd/UsdError.hh index 7078d4ef2..28742e2d6 100644 --- a/usd/include/sdf/usd/UsdError.hh +++ b/usd/include/sdf/usd/UsdError.hh @@ -66,6 +66,9 @@ namespace sdf /// \brief Invalid submesh primitive type INVALID_SUBMESH_PRIMITIVE_TYPE, + + /// \brief Invalid material + INVALID_MATERIAL, }; class IGNITION_SDFORMAT_USD_VISIBLE UsdError diff --git a/usd/include/sdf/usd/sdf_parser/Material.hh b/usd/include/sdf/usd/sdf_parser/Material.hh new file mode 100644 index 000000000..429dd7b8e --- /dev/null +++ b/usd/include/sdf/usd/sdf_parser/Material.hh @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#ifndef SDF_USD_SDF_PARSER_MATERIALS_HH_ +#define SDF_USD_SDF_PARSER_MATERIALS_HH_ + +// TODO(ahcorde) this is to remove deprecated "warnings" in usd, these warnings +// are reported using #pragma message so normal diagnostic flags cannot remove +// them. This workaround requires this block to be used whenever usd is +// included. +#pragma push_macro ("__DEPRECATED") +#undef __DEPRECATED +#include +#include +#include +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/Material.hh" +#include "sdf/usd/Export.hh" +#include "sdf/usd/UsdError.hh" +#include "sdf/sdf_config.h" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief Parse an SDF material into a USD stage. + /// \param[in] _materialSdf The SDF material to parse. + /// \param[in] _stage The stage that should contain the USD representation + /// of _material. + /// \param[out] _materialPath USD Material path + /// \return UsdErrors, which is a list of UsdError objects. This list is + /// empty if no errors occurred when parsing _materialSdf its USD + /// representation + UsdErrors IGNITION_SDFORMAT_USD_VISIBLE ParseSdfMaterial( + const sdf::Material *_materialSdf, + pxr::UsdStageRefPtr &_stage, + pxr::SdfPath &_materialPath); + } + } +} + +#endif diff --git a/usd/src/CMakeLists.txt b/usd/src/CMakeLists.txt index 72db29aad..29b4ef4b5 100644 --- a/usd/src/CMakeLists.txt +++ b/usd/src/CMakeLists.txt @@ -1,9 +1,11 @@ set(sources + Conversions.cc UsdError.cc sdf_parser/Geometry.cc sdf_parser/Joint.cc sdf_parser/Light.cc sdf_parser/Link.cc + sdf_parser/Material.cc sdf_parser/Model.cc sdf_parser/Sensor.cc sdf_parser/Visual.cc @@ -30,11 +32,13 @@ set(gtest_sources sdf_parser/Joint_Sdf2Usd_TEST.cc sdf_parser/Light_Sdf2Usd_TEST.cc sdf_parser/Link_Sdf2Usd_TEST.cc + sdf_parser/Material_Sdf2Usd_TEST.cc # TODO(adlarkin) add a test for SDF -> USD models once model parsing # functionality is complete sdf_parser/Sensors_Sdf2Usd_TEST.cc sdf_parser/Visual_Sdf2Usd_TEST.cc sdf_parser/World_Sdf2Usd_TEST.cc + Conversions_TEST.cc UsdError_TEST.cc UsdUtils_TEST.cc ) diff --git a/usd/src/Conversions.cc b/usd/src/Conversions.cc new file mode 100644 index 000000000..5386893b3 --- /dev/null +++ b/usd/src/Conversions.cc @@ -0,0 +1,152 @@ +/* + * Copyright 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include "sdf/usd/Conversions.hh" + +#include + +#include "sdf/Pbr.hh" + +namespace sdf +{ + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + sdf::Material convert(const ignition::common::Material *_in) + { + sdf::Material out; + out.SetEmissive(_in->Emissive()); + out.SetDiffuse(_in->Diffuse()); + out.SetSpecular(_in->Specular()); + out.SetAmbient(_in->Ambient()); + out.SetRenderOrder(_in->RenderOrder()); + out.SetLighting(_in->Lighting()); + out.SetDoubleSided(_in->TwoSidedEnabled()); + const ignition::common::Pbr * pbr = _in->PbrMaterial(); + if (pbr != nullptr) + { + out.SetNormalMap(pbr->NormalMap()); + sdf::Pbr pbrOut; + sdf::PbrWorkflow pbrWorkflow; + pbrWorkflow.SetAlbedoMap(pbr->AlbedoMap()); + pbrWorkflow.SetMetalnessMap(pbr->MetalnessMap()); + pbrWorkflow.SetEmissiveMap(pbr->EmissiveMap()); + pbrWorkflow.SetRoughnessMap(pbr->RoughnessMap()); + pbrWorkflow.SetSpecularMap(pbr->SpecularMap()); + pbrWorkflow.SetEnvironmentMap(pbr->EnvironmentMap()); + pbrWorkflow.SetAmbientOcclusionMap(pbr->AmbientOcclusionMap()); + pbrWorkflow.SetLightMap(pbr->LightMap()); + pbrWorkflow.SetRoughness(pbr->Roughness()); + pbrWorkflow.SetGlossiness(pbr->Glossiness()); + pbrWorkflow.SetMetalness(pbr->Metalness()); + + if (pbr->NormalMapType() == ignition::common::NormalMapSpace::TANGENT) + { + pbrWorkflow.SetNormalMap( + pbr->NormalMap(), sdf::NormalMapSpace::TANGENT); + } + else + { + pbrWorkflow.SetNormalMap( + pbr->NormalMap(), sdf::NormalMapSpace::OBJECT); + } + + if (pbr->Type() == ignition::common::PbrType::METAL) + { + pbrOut.SetWorkflow(sdf::PbrWorkflowType::METAL, pbrWorkflow); + } + else if (pbr->Type() == ignition::common::PbrType::SPECULAR) + { + pbrOut.SetWorkflow(sdf::PbrWorkflowType::SPECULAR, pbrWorkflow); + } + out.SetPbrMaterial(pbrOut); + } + else if (!_in->TextureImage().empty()) + { + sdf::Pbr pbrOut; + sdf::PbrWorkflow pbrWorkflow; + pbrWorkflow.SetAlbedoMap(_in->TextureImage()); + pbrOut.SetWorkflow(sdf::PbrWorkflowType::SPECULAR, pbrWorkflow); + out.SetPbrMaterial(pbrOut); + } + + return out; + } + + void convert(const sdf::Material &_in, ignition::common::Material &_out) + { + _out.SetEmissive(_in.Emissive()); + _out.SetDiffuse(_in.Diffuse()); + _out.SetSpecular(_in.Specular()); + _out.SetAmbient(_in.Ambient()); + _out.SetRenderOrder(_in.RenderOrder()); + _out.SetLighting(_in.Lighting()); + _out.SetAlphaFromTexture(false, 0.5, _in.DoubleSided()); + + const sdf::Pbr * pbr = _in.PbrMaterial(); + if (pbr != nullptr) + { + ignition::common::Pbr pbrOut; + + const sdf::PbrWorkflow * pbrWorkflow = + pbr->Workflow(sdf::PbrWorkflowType::METAL); + if (pbrWorkflow) + { + pbrOut.SetType(ignition::common::PbrType::METAL); + } + else + { + pbrWorkflow = pbr->Workflow(sdf::PbrWorkflowType::SPECULAR); + if (pbrWorkflow) + { + pbrOut.SetType(ignition::common::PbrType::SPECULAR); + } + } + if (pbrWorkflow != nullptr) + { + pbrOut.SetAlbedoMap(pbrWorkflow->AlbedoMap()); + pbrOut.SetMetalnessMap(pbrWorkflow->MetalnessMap()); + pbrOut.SetEmissiveMap(pbrWorkflow->EmissiveMap()); + pbrOut.SetRoughnessMap(pbrWorkflow->RoughnessMap()); + pbrOut.SetSpecularMap(pbrWorkflow->SpecularMap()); + pbrOut.SetEnvironmentMap(pbrWorkflow->EnvironmentMap()); + pbrOut.SetAmbientOcclusionMap(pbrWorkflow->AmbientOcclusionMap()); + pbrOut.SetLightMap(pbrWorkflow->LightMap()); + pbrOut.SetRoughness(pbrWorkflow->Roughness()); + pbrOut.SetGlossiness(pbrWorkflow->Glossiness()); + pbrOut.SetMetalness(pbrWorkflow->Metalness()); + + if (pbrWorkflow->NormalMapType() == sdf::NormalMapSpace::TANGENT) + { + pbrOut.SetNormalMap( + pbrWorkflow->NormalMap(), + ignition::common::NormalMapSpace::TANGENT); + } + else if (pbrWorkflow->NormalMapType() == sdf::NormalMapSpace::OBJECT) + { + pbrOut.SetNormalMap( + pbrWorkflow->NormalMap(), + ignition::common::NormalMapSpace::OBJECT); + } + } + _out.SetPbrMaterial(pbrOut); + } + } + } + } +} diff --git a/usd/src/Conversions_TEST.cc b/usd/src/Conversions_TEST.cc new file mode 100644 index 000000000..e5c092146 --- /dev/null +++ b/usd/src/Conversions_TEST.cc @@ -0,0 +1,161 @@ +/* + * Copyright 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include + +#include +#include +#include + +#include "sdf/Material.hh" +#include "sdf/Pbr.hh" +#include "sdf/usd/Conversions.hh" + +///////////////////////////////////////////////// +TEST(Conversions, SdfToCommonMaterial) +{ + sdf::Material material; + material.SetEmissive(ignition::math::Color(1, 0.2, 0.2, 0.7)); + material.SetDiffuse(ignition::math::Color(0.1, 0.3, 0.4, 0.5)); + material.SetSpecular(ignition::math::Color(0.11, 0.22, 0.23, 0.77)); + material.SetAmbient(ignition::math::Color(0.25, 0.21, 0.28, 0.5)); + material.SetRenderOrder(5); + material.SetLighting(true); + material.SetDoubleSided(false); + + sdf::Pbr pbrSDF; + sdf::PbrWorkflow pbrWorkflow; + + pbrWorkflow.SetAlbedoMap("AlbedoMap"); + pbrWorkflow.SetMetalnessMap("MetalnessMap"); + pbrWorkflow.SetEmissiveMap("EmissiveMap"); + pbrWorkflow.SetRoughnessMap("RoughnessMap"); + pbrWorkflow.SetSpecularMap("SpecularMap"); + pbrWorkflow.SetEnvironmentMap("EnvironmentMap"); + pbrWorkflow.SetAmbientOcclusionMap("AmbientOcclusionMap"); + pbrWorkflow.SetLightMap("LightMap"); + pbrWorkflow.SetNormalMap( + "NormalMap", sdf::NormalMapSpace::TANGENT); + pbrWorkflow.SetRoughness(0.2); + pbrWorkflow.SetGlossiness(0.3); + pbrWorkflow.SetMetalness(0.55); + + pbrSDF.SetWorkflow(sdf::PbrWorkflowType::METAL, pbrWorkflow); + material.SetPbrMaterial(pbrSDF); + + ignition::common::Material materialCommon; + sdf::usd::convert(material, materialCommon); + const ignition::common::Pbr * pbrCommon = materialCommon.PbrMaterial(); + ASSERT_NE(nullptr, pbrCommon); + + EXPECT_EQ(material.Emissive(), materialCommon.Emissive()); + EXPECT_EQ(material.Diffuse(), materialCommon.Diffuse()); + EXPECT_EQ(material.Specular(), materialCommon.Specular()); + EXPECT_EQ(material.Ambient(), materialCommon.Ambient()); + EXPECT_FLOAT_EQ(material.RenderOrder(), + static_cast(materialCommon.RenderOrder())); + EXPECT_EQ(material.Lighting(), materialCommon.Lighting()); + EXPECT_EQ(material.DoubleSided(), materialCommon.TwoSidedEnabled()); + + EXPECT_EQ(pbrWorkflow.AlbedoMap(), pbrCommon->AlbedoMap()); + EXPECT_EQ(pbrWorkflow.MetalnessMap(), pbrCommon->MetalnessMap()); + EXPECT_EQ(pbrWorkflow.EmissiveMap(), pbrCommon->EmissiveMap()); + EXPECT_EQ(pbrWorkflow.RoughnessMap(), pbrCommon->RoughnessMap()); + EXPECT_EQ(pbrWorkflow.SpecularMap(), pbrCommon->SpecularMap()); + EXPECT_EQ(pbrWorkflow.EnvironmentMap(), pbrCommon->EnvironmentMap()); + EXPECT_EQ(pbrWorkflow.AmbientOcclusionMap(), + pbrCommon->AmbientOcclusionMap()); + EXPECT_EQ(pbrWorkflow.LightMap(), pbrCommon->LightMap()); + EXPECT_EQ(pbrWorkflow.NormalMap(), pbrCommon->NormalMap()); + + EXPECT_EQ(ignition::common::NormalMapSpace::TANGENT, + pbrCommon->NormalMapType()); + EXPECT_EQ(ignition::common::PbrType::METAL, pbrCommon->Type()); + + EXPECT_DOUBLE_EQ(pbrWorkflow.Roughness(), pbrCommon->Roughness()); + EXPECT_DOUBLE_EQ(pbrWorkflow.Glossiness(), pbrCommon->Glossiness()); + EXPECT_DOUBLE_EQ(pbrWorkflow.Metalness(), pbrCommon->Metalness()); +} + +TEST(Conversions, CommonToSdfMaterial) +{ + ignition::common::Material materialCommon; + materialCommon.SetEmissive(ignition::math::Color(1, 0.2, 0.2, 0.7)); + materialCommon.SetDiffuse(ignition::math::Color(0.1, 0.3, 0.4, 0.5)); + materialCommon.SetSpecular(ignition::math::Color(0.11, 0.22, 0.23, 0.77)); + materialCommon.SetAmbient(ignition::math::Color(0.25, 0.21, 0.28, 0.5)); + materialCommon.SetRenderOrder(5); + materialCommon.SetLighting(true); + materialCommon.SetAlphaFromTexture(false, 0.5, true); + + ignition::common::Pbr pbrCommon; + pbrCommon.SetType(ignition::common::PbrType::METAL); + + pbrCommon.SetAlbedoMap("AlbedoMap"); + pbrCommon.SetMetalnessMap("MetalnessMap"); + pbrCommon.SetEmissiveMap("EmissiveMap"); + pbrCommon.SetRoughnessMap("RoughnessMap"); + pbrCommon.SetSpecularMap("SpecularMap"); + pbrCommon.SetEnvironmentMap("EnvironmentMap"); + pbrCommon.SetAmbientOcclusionMap("AmbientOcclusionMap"); + pbrCommon.SetLightMap("LightMap"); + pbrCommon.SetNormalMap( + "NormalMap", ignition::common::NormalMapSpace::TANGENT); + pbrCommon.SetRoughness(0.2); + pbrCommon.SetGlossiness(0.3); + pbrCommon.SetMetalness(0.55); + + materialCommon.SetPbrMaterial(pbrCommon); + + const sdf::Material material = sdf::usd::convert(&materialCommon); + + const sdf::Pbr * pbr = material.PbrMaterial(); + ASSERT_NE(nullptr, pbr); + const sdf::PbrWorkflow * pbrWorkflow = + pbr->Workflow(sdf::PbrWorkflowType::METAL); + ASSERT_NE(nullptr, pbrWorkflow); + + EXPECT_EQ(material.Emissive(), materialCommon.Emissive()); + EXPECT_EQ(material.Diffuse(), materialCommon.Diffuse()); + EXPECT_EQ(material.Specular(), materialCommon.Specular()); + EXPECT_EQ(material.Ambient(), materialCommon.Ambient()); + EXPECT_FLOAT_EQ(material.RenderOrder(), + static_cast(materialCommon.RenderOrder())); + EXPECT_EQ(material.Lighting(), materialCommon.Lighting()); + EXPECT_EQ(material.DoubleSided(), materialCommon.TwoSidedEnabled()); + + EXPECT_EQ(pbrWorkflow->AlbedoMap(), pbrCommon.AlbedoMap()); + EXPECT_EQ(pbrWorkflow->MetalnessMap(), pbrCommon.MetalnessMap()); + EXPECT_EQ(pbrWorkflow->EmissiveMap(), pbrCommon.EmissiveMap()); + EXPECT_EQ(pbrWorkflow->RoughnessMap(), pbrCommon.RoughnessMap()); + EXPECT_EQ(pbrWorkflow->SpecularMap(), pbrCommon.SpecularMap()); + EXPECT_EQ(pbrWorkflow->EnvironmentMap(), pbrCommon.EnvironmentMap()); + EXPECT_EQ(pbrWorkflow->AmbientOcclusionMap(), + pbrCommon.AmbientOcclusionMap()); + EXPECT_EQ(pbrWorkflow->LightMap(), pbrCommon.LightMap()); + EXPECT_EQ(pbrWorkflow->NormalMap(), pbrCommon.NormalMap()); + + EXPECT_EQ(ignition::common::NormalMapSpace::TANGENT, + pbrCommon.NormalMapType()); + EXPECT_EQ(ignition::common::PbrType::METAL, pbrCommon.Type()); + + EXPECT_DOUBLE_EQ(pbrWorkflow->Roughness(), pbrCommon.Roughness()); + EXPECT_DOUBLE_EQ(pbrWorkflow->Glossiness(), pbrCommon.Glossiness()); + EXPECT_DOUBLE_EQ(pbrWorkflow->Metalness(), pbrCommon.Metalness()); +} diff --git a/usd/src/cmd/sdf2usd.cc b/usd/src/cmd/sdf2usd.cc index 06bc77980..5ca0d7131 100644 --- a/usd/src/cmd/sdf2usd.cc +++ b/usd/src/cmd/sdf2usd.cc @@ -17,11 +17,15 @@ #include -// TODO(ahcorde):this is to remove deprecated "warnings" in usd, these warnings +#include +#include + +#include + +// TODO(ahcorde) this is to remove deprecated "warnings" in usd, these warnings // are reported using #pragma message so normal diagnostic flags cannot remove // them. This workaround requires this block to be used whenever usd is // included. -#include #pragma push_macro ("__DEPRECATED") #undef __DEPRECATED #include @@ -51,8 +55,164 @@ struct Options std::string outputFilename{"output.usd"}; }; +////////////////////////////////////////////////// +/// \brief Get the full path of a file +/// \param[in] _path Where to begin searching for the file +/// \param[in] _name The name of the file to find +/// \return The full path to the file named _name. Empty string is returned if +/// the file could not be found. +std::string findFileByName(const std::string &_path, const std::string &_name) +{ + for (ignition::common::DirIter file(_path); + file != ignition::common::DirIter(); ++file) + { + std::string current(*file); + + if (ignition::common::isDirectory(current)) + { + std::string result = findFileByName(current, _name); + if (result.empty()) + { + continue; + } + return result; + } + + if (!ignition::common::isFile(current)) + continue; + + auto fileName = ignition::common::basename(current); + + if (fileName == _name) + { + return current; + } + } + return ""; +} + +////////////////////////////////////////////////// +/// \brief Get the full path of a file based on the extension +/// \param[in] _path Where to begin searching for the file +/// \param[in] _extension The extension of the file +/// \param[in] _insertDirectories Whether subdirectories should be inserted as +/// needed when looking for the file (true) or not (false) +/// \return The full path to the file with an extension _extension. Empty +/// string is returned if the file could not be found. +std::string findFileByExtension( + const std::string &_path, const std::string &_extension, + bool _insertDirectories = false) +{ + if (_insertDirectories) + { + for (ignition::common::DirIter file(_path); + file != ignition::common::DirIter(); ++file) + { + std::string current(*file); + if (ignition::common::isDirectory(current)) + { + auto systemPaths = ignition::common::systemPaths(); + systemPaths->AddFilePaths(current); + findFileByExtension(current, "", true); + } + } + } + + for (ignition::common::DirIter file(_path); + file != ignition::common::DirIter(); ++file) + { + std::string current(*file); + + if (ignition::common::isDirectory(current)) + { + std::string result = findFileByExtension(current, _extension); + if (result.empty()) + { + continue; + } + return result; + } + + if (!ignition::common::isFile(current)) + continue; + + auto fileName = ignition::common::basename(current); + auto fileExtensionIndex = fileName.rfind("."); + auto fileExtension = fileName.substr(fileExtensionIndex + 1); + + if (fileExtension == _extension) + { + return current; + } + } + return ""; +} + +////////////////////////////////////////////////// +/// \brief This functions is used by sdf::setFindCallback to find +/// the resources defined in the URI +/// \param[in] _uri URI of the file to find +/// \return The full path to the uri. Empty +/// string is returned if the file could not be found. +std::string FindResources(const std::string &_uri) +{ + ignition::common::URI uri(_uri); + std::string path; + std::string home; + if (!ignition::common::env("HOME", home, false)) + { + std::cerr << "The HOME environment variable was not defined, " + << "so the resource [" << _uri << "] could not be found\n"; + return ""; + } + if (uri.Scheme() == "http" || uri.Scheme() == "https") + { + std::vector tokens = + ignition::common::split(uri.Path().Str(), "/"); + const std::string server = tokens[0]; + const std::string versionServer = tokens[1]; + const std::string owner = ignition::common::lowercase(tokens[2]); + const std::string type = ignition::common::lowercase(tokens[3]); + const std::string modelName = ignition::common::lowercase(tokens[4]); + path = ignition::common::joinPaths( + home, ".ignition", "fuel", server, owner, type, modelName); + } + else + { + path = ignition::common::joinPaths(home, ".ignition", "fuel"); + } + + auto fileName = ignition::common::basename(uri.Path().Str()); + auto fileExtensionIndex = fileName.rfind("."); + if (fileExtensionIndex == std::string::npos) + { + return findFileByExtension(path, "sdf", true); + } + else + { + return findFileByName(path, fileName); + } + return ""; +} + +////////////////////////////////////////////////// +/// \brief This function is used by ignition::common::addFindFileURICallback to +/// find the resources defined in the URI +/// \param[in] _uri URI of the file to find +/// \return The full path to the uri. Empty +/// string is returned if the file could not be found. +std::string FindResourceUri(const ignition::common::URI &_uri) +{ + return FindResources(_uri.Str()); +} + void runCommand(const Options &_opt) { + // Configure SDF to fetch assets from ignition fuel. + sdf::setFindCallback(std::bind(&FindResources, std::placeholders::_1)); + ignition::common::addFindFileURICallback( + std::bind(&FindResourceUri, std::placeholders::_1)); + sdf::Root root; auto errors = root.Load(_opt.inputFilename); if (!errors.empty()) diff --git a/usd/src/sdf_parser/Geometry.cc b/usd/src/sdf_parser/Geometry.cc index dde5e43b5..10912ca4f 100644 --- a/usd/src/sdf_parser/Geometry.cc +++ b/usd/src/sdf_parser/Geometry.cc @@ -58,6 +58,9 @@ #include "sdf/Mesh.hh" #include "sdf/Plane.hh" #include "sdf/Sphere.hh" +#include "sdf/usd/Conversions.hh" +#include "sdf/usd/sdf_parser/Material.hh" + #include "../UsdUtils.hh" namespace sdf @@ -380,6 +383,56 @@ namespace usd pxr::GfVec3f(meshMax.X(), meshMax.Y(), meshMax.Z())); usdMesh.CreateExtentAttr().Set(extentBounds); + // TODO(adlarkin) update this call in sdf13 to avoid casting the index to + // an int: + // https://github.com/ignitionrobotics/ign-common/pull/319 + int materialIndex = subMesh->MaterialIndex(); + if (materialIndex != -1) + { + const auto material = ignMesh->MaterialByIndex(materialIndex).get(); + const sdf::Material materialSdf = sdf::usd::convert(material); + pxr::SdfPath materialPath; + UsdErrors materialErrors = ParseSdfMaterial( + &materialSdf, _stage, materialPath); + if (!materialErrors.empty()) + { + errors.push_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Unable to convert material [" + std::to_string(materialIndex) + + "] of submesh named [" + subMesh->Name() + + "] to a USD material.")); + return errors; + } + + auto materialPrim = _stage->GetPrimAtPath(materialPath); + if (!materialPrim) + { + errors.push_back(UsdError( + sdf::usd::UsdErrorCode::INVALID_PRIM_PATH, + "Unable to get material prim at path [" + + materialPath.GetString() + + "], but a prim should exist at this path.")); + return errors; + } + + auto materialUSD = pxr::UsdShadeMaterial(materialPrim); + if (materialUSD && + (materialSdf.Emissive() != ignition::math::Color(0, 0, 0, 1) + || materialSdf.Specular() != ignition::math::Color(0, 0, 0, 1) + || materialSdf.PbrMaterial())) + { + pxr::UsdShadeMaterialBindingAPI(usdMesh).Bind(materialUSD); + } + else if (!materialUSD) + { + errors.push_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "The prim at path [" + materialPath.GetString() + + "] is not a pxr::UsdShadeMaterial object.")); + return errors; + } + } + pxr::UsdGeomXformCommonAPI xform(usdMesh); ignition::math::Vector3d scale = _geometry.MeshShape()->Scale(); xform.SetScale(pxr::GfVec3f{ diff --git a/usd/src/sdf_parser/Material.cc b/usd/src/sdf_parser/Material.cc new file mode 100644 index 000000000..0a1c789a8 --- /dev/null +++ b/usd/src/sdf_parser/Material.cc @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include "sdf/usd/sdf_parser/Material.hh" + +#include +#include + +#include +#include +#include + +#include + +// TODO(ahcorde) this is to remove deprecated "warnings" in usd, these warnings +// are reported using #pragma message so normal diagnostic flags cannot remove +// them. This workaround requires this block to be used whenever usd is +// included. +#pragma push_macro ("__DEPRECATED") +#undef __DEPRECATED +#include +#include +#include +#include +#include +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/Pbr.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +// +namespace usd +{ + /// \brief Copy a file in a directory + /// \param[in] _path path where the copy will be located + /// \param[in] _fullPath name of the file to copy + /// \return True if the file at _fullPath was copied to _path. False otherwise + bool copyMaterial(const std::string &_path, const std::string &_fullPath) + { + if (!_path.empty() && !_fullPath.empty()) + { + auto fileName = ignition::common::basename(_path); + auto filePathIndex = _path.rfind(fileName); + auto filePath = _path.substr(0, filePathIndex); + if (!filePath.empty()) + { + ignition::common::createDirectories(filePath); + } + return ignition::common::copyFile(_fullPath, _path); + } + return false; + } + + /// \brief Get the path to copy the material to + /// \param[in] _uri full path of the file to copy + /// \return A relative path to save the material. The path looks like: + /// materials/textures/ + std::string getMaterialCopyPath(const std::string &_uri) + { + return ignition::common::joinPaths( + "materials", + "textures", + ignition::common::basename(_uri)); + } + + /// \brief Fill Material shader attributes and properties + /// \param[in] _prim USD primitive + /// \param[in] _name Name of the field attribute or property + /// \param[in] _vType Type of the field + /// \param[in] _value Value of the field + /// \param[in] _customData Custom data to set the field + /// \param[in] _displayName Display name + /// \param[in] _displayGroup Display group + /// \param[in] _doc Documentation of the field + /// \param[in] _colorSpace if the material is a texture, we can specify the + /// color space of the image + /// \return UsdErrors, which is a list of UsdError objects. This list is empty + /// if no errors occurred when creating the material input. + template + UsdErrors CreateMaterialInput( + const pxr::UsdPrim &_prim, + const std::string &_name, + const pxr::SdfValueTypeName &_vType, + const T &_value, + const std::map &_customData, + const pxr::TfToken &_displayName, + const pxr::TfToken &_displayGroup, + const std::string &_doc, + const pxr::TfToken &_colorSpace = pxr::TfToken("")) + { + UsdErrors errors; + auto shader = pxr::UsdShadeShader(_prim); + if (!shader) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::INVALID_PRIM_PATH, + "Unable to convert the prim to a UsdShadeShader")); + return errors; + } + + auto existingInput = shader.GetInput(pxr::TfToken(_name)); + pxr::SdfValueTypeName vTypeName; + if (_vType.IsScalar()) + { + vTypeName = _vType.GetScalarType(); + } + else if (_vType.IsArray()) + { + vTypeName = _vType.GetArrayType(); + } + auto surfaceInput = shader.CreateInput( + pxr::TfToken(_name), vTypeName); + surfaceInput.Set(_value); + auto attr = surfaceInput.GetAttr(); + + for (const auto &[key, customValue] : _customData) + { + attr.SetCustomDataByKey(key, customValue); + } + if (!_displayName.GetString().empty()) + { + attr.SetDisplayName(_displayName); + } + if (!_displayGroup.GetString().empty()) + { + attr.SetDisplayGroup(_displayGroup); + } + if (!_doc.empty()) + { + attr.SetDocumentation(_doc); + } + if (!_colorSpace.GetString().empty()) + { + attr.SetColorSpace(_colorSpace); + } + return errors; + } + + UsdErrors ParseSdfMaterial(const sdf::Material *_materialSdf, + pxr::UsdStageRefPtr &_stage, pxr::SdfPath &_materialPath) + { + UsdErrors errors; + + const auto looksPath = pxr::SdfPath("/Looks"); + auto looksPrim = _stage->GetPrimAtPath(looksPath); + if (!looksPrim) + { + looksPrim = _stage->DefinePrim(looksPath, pxr::TfToken("Scope")); + } + + // This variable will increase with every new material to avoid collision + // with the names of the materials + static int i = 0; + + _materialPath = pxr::SdfPath("/Looks/Material_" + std::to_string(i)); + i++; + + pxr::UsdShadeMaterial materialUsd; + auto usdMaterialPrim = _stage->GetPrimAtPath(_materialPath); + if (!usdMaterialPrim) + { + materialUsd = pxr::UsdShadeMaterial::Define(_stage, _materialPath); + } + else + { + materialUsd = pxr::UsdShadeMaterial(usdMaterialPrim); + } + + const auto shaderPath = pxr::SdfPath(_materialPath.GetString() + "/Shader"); + auto usdShader = pxr::UsdShadeShader::Define(_stage, shaderPath); + auto shaderPrim = _stage->GetPrimAtPath(shaderPath); + if (!shaderPrim) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::INVALID_PRIM_PATH, + "Not able to cast the UsdShadeShader at path [" + shaderPath.GetString() + + "] to a Prim")); + } + + auto shaderOut = pxr::UsdShadeConnectableAPI(shaderPrim).CreateOutput( + pxr::TfToken("out"), pxr::SdfValueTypeNames->Token); + const auto mdlToken = pxr::TfToken("mdl"); + materialUsd.CreateSurfaceOutput(mdlToken).ConnectToSource(shaderOut); + materialUsd.CreateVolumeOutput(mdlToken).ConnectToSource(shaderOut); + materialUsd.CreateDisplacementOutput(mdlToken).ConnectToSource(shaderOut); + usdShader.GetImplementationSourceAttr().Set( + pxr::UsdShadeTokens->sourceAsset); + usdShader.SetSourceAsset(pxr::SdfAssetPath("OmniPBR.mdl"), mdlToken); + usdShader.SetSourceAssetSubIdentifier(pxr::TfToken("OmniPBR"), mdlToken); + + const std::map customDataDiffuse = + { + {pxr::TfToken("default"), pxr::VtValue(pxr::GfVec3f(0.2, 0.2, 0.2))}, + {pxr::TfToken("range:max"), + pxr::VtValue(pxr::GfVec3f(100000, 100000, 100000))}, + {pxr::TfToken("range:min"), pxr::VtValue(pxr::GfVec3f(0, 0, 0))} + }; + const ignition::math::Color diffuse = _materialSdf->Diffuse(); + auto errorsMaterialDiffuseColorConstant = CreateMaterialInput( + shaderPrim, + "diffuse_color_constant", + pxr::SdfValueTypeNames->Color3f, + pxr::GfVec3f(diffuse.R(), diffuse.G(), diffuse.B()), + customDataDiffuse, + pxr::TfToken("Base Color"), + pxr::TfToken("Albedo"), + "This is the base color"); + + if (!errorsMaterialDiffuseColorConstant.empty()) + { + errors.insert( + errors.end(), + errorsMaterialDiffuseColorConstant.begin(), + errorsMaterialDiffuseColorConstant.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the base color of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + + const std::map customDataEmissive = + { + {pxr::TfToken("default"), pxr::VtValue(pxr::GfVec3f(1, 0.1, 0.1))}, + {pxr::TfToken("range:max"), + pxr::VtValue(pxr::GfVec3f(100000, 100000, 100000))}, + {pxr::TfToken("range:min"), pxr::VtValue(pxr::GfVec3f(0, 0, 0))} + }; + ignition::math::Color emissive = _materialSdf->Emissive(); + auto errorsMaterialEmissiveColor = CreateMaterialInput( + shaderPrim, + "emissive_color", + pxr::SdfValueTypeNames->Color3f, + pxr::GfVec3f(emissive.R(), emissive.G(), emissive.B()), + customDataEmissive, + pxr::TfToken("Emissive Color"), + pxr::TfToken("Emissive"), + "The emission color"); + + if (!errorsMaterialEmissiveColor.empty()) + { + errors.insert( + errors.end(), + errorsMaterialEmissiveColor.begin(), + errorsMaterialEmissiveColor.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the emission color of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + + const std::map customDataEnableEmission = + { + {pxr::TfToken("default"), pxr::VtValue(0)} + }; + + auto errorsMaterialEnableEmission = CreateMaterialInput( + shaderPrim, + "enable_emission", + pxr::SdfValueTypeNames->Bool, + emissive.A() > 0, + customDataEnableEmission, + pxr::TfToken("Enable Emissive"), + pxr::TfToken("Emissive"), + "Enables the emission of light from the material"); + + if (!errorsMaterialEnableEmission.empty()) + { + errors.insert( + errors.end(), + errorsMaterialEnableEmission.begin(), + errorsMaterialEnableEmission.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the emissive enabled propery of the material at path" + " [" + _materialPath.GetString() + "]")); + return errors; + } + + const std::map customDataIntensity = + { + {pxr::TfToken("default"), pxr::VtValue(40)}, + {pxr::TfToken("range:max"), pxr::VtValue(100000)}, + {pxr::TfToken("range:min"), pxr::VtValue(0)} + }; + auto errorsMaterialEmissiveIntensity = CreateMaterialInput( + shaderPrim, + "emissive_intensity", + pxr::SdfValueTypeNames->Float, + emissive.A(), + customDataIntensity, + pxr::TfToken("Emissive Intensity"), + pxr::TfToken("Emissive"), + "Intensity of the emission"); + + if (!errorsMaterialEmissiveIntensity.empty()) + { + errors.insert( + errors.end(), + errorsMaterialEmissiveIntensity.begin(), + errorsMaterialEmissiveIntensity.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the emissive intensity of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + + const sdf::Pbr * pbr = _materialSdf->PbrMaterial(); + if (pbr) + { + const sdf::PbrWorkflow * pbrWorkflow = + pbr->Workflow(sdf::PbrWorkflowType::METAL); + if (!pbrWorkflow) + { + pbrWorkflow = pbr->Workflow(sdf::PbrWorkflowType::SPECULAR); + } + + if (pbrWorkflow) + { + const std::map customDataMetallicConstant = + { + {pxr::TfToken("default"), pxr::VtValue(0.5)}, + {pxr::TfToken("range:max"), pxr::VtValue(1)}, + {pxr::TfToken("range:min"), pxr::VtValue(0)} + }; + auto errorsMaterialMetallicConstant = CreateMaterialInput( + shaderPrim, + "metallic_constant", + pxr::SdfValueTypeNames->Float, + pbrWorkflow->Metalness(), + customDataMetallicConstant, + pxr::TfToken("Metallic Amount"), + pxr::TfToken("Reflectivity"), + "Metallic Material"); + if (!errorsMaterialMetallicConstant.empty()) + { + errors.insert( + errors.end(), + errorsMaterialMetallicConstant.begin(), + errorsMaterialMetallicConstant.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the metallic constant of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + const std::map customDataRoughnessConstant = + { + {pxr::TfToken("default"), pxr::VtValue(0.5)}, + {pxr::TfToken("range:max"), pxr::VtValue(1)}, + {pxr::TfToken("range:min"), pxr::VtValue(0)} + }; + auto errorsMaterialReflectionRoughnessConstant = + CreateMaterialInput( + shaderPrim, + "reflection_roughness_constant", + pxr::SdfValueTypeNames->Float, + pbrWorkflow->Roughness(), + customDataRoughnessConstant, + pxr::TfToken("Roughness Amount"), + pxr::TfToken("Reflectivity"), + "Higher roughness values lead to more blurry reflections"); + if (!errorsMaterialReflectionRoughnessConstant.empty()) + { + errors.insert( + errors.end(), + errorsMaterialReflectionRoughnessConstant.begin(), + errorsMaterialReflectionRoughnessConstant.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the roughness constant of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + + const std::map customDefaultSdfAssetPath = + { + {pxr::TfToken("default"), pxr::VtValue(pxr::SdfAssetPath())}, + }; + + if (!pbrWorkflow->AlbedoMap().empty()) + { + std::string copyPath = getMaterialCopyPath(pbrWorkflow->AlbedoMap()); + + std::string fullnameAlbedoMap = + ignition::common::findFile( + ignition::common::basename(pbrWorkflow->AlbedoMap())); + + if (fullnameAlbedoMap.empty()) + { + fullnameAlbedoMap = pbrWorkflow->AlbedoMap(); + } + + copyMaterial(copyPath, fullnameAlbedoMap); + + auto errorsMaterialDiffuseTexture = + CreateMaterialInput( + shaderPrim, + "diffuse_texture", + pxr::SdfValueTypeNames->Asset, + pxr::SdfAssetPath(copyPath), + customDefaultSdfAssetPath, + pxr::TfToken("Base Map"), + pxr::TfToken("Albedo"), + "", + pxr::TfToken("auto")); + if (!errorsMaterialDiffuseTexture.empty()) + { + errors.insert( + errors.end(), + errorsMaterialDiffuseTexture.begin(), + errorsMaterialDiffuseTexture.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the albedo of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + } + if (!pbrWorkflow->MetalnessMap().empty()) + { + std::string copyPath = + getMaterialCopyPath(pbrWorkflow->MetalnessMap()); + + std::string fullnameMetallnessMap = + ignition::common::findFile( + ignition::common::basename(pbrWorkflow->MetalnessMap())); + + if (fullnameMetallnessMap.empty()) + { + fullnameMetallnessMap = pbrWorkflow->MetalnessMap(); + } + + copyMaterial(copyPath, fullnameMetallnessMap); + + auto errorsMaterialMetallicTexture = + CreateMaterialInput( + shaderPrim, + "metallic_texture", + pxr::SdfValueTypeNames->Asset, + pxr::SdfAssetPath(copyPath), + customDefaultSdfAssetPath, + pxr::TfToken("Metallic Map"), + pxr::TfToken("Reflectivity"), + "", + pxr::TfToken("raw")); + if (!errorsMaterialMetallicTexture.empty()) + { + errors.insert( + errors.end(), + errorsMaterialMetallicTexture.begin(), + errorsMaterialMetallicTexture.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the reflectivity of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + } + if (!pbrWorkflow->NormalMap().empty()) + { + std::string copyPath = getMaterialCopyPath(pbrWorkflow->NormalMap()); + + std::string fullnameNormalMap = + ignition::common::findFile( + ignition::common::basename(pbrWorkflow->NormalMap())); + + if (fullnameNormalMap.empty()) + { + fullnameNormalMap = pbrWorkflow->NormalMap(); + } + + copyMaterial(copyPath, fullnameNormalMap); + + auto errorsMaterialNormalMapTexture = + CreateMaterialInput( + shaderPrim, + "normalmap_texture", + pxr::SdfValueTypeNames->Asset, + pxr::SdfAssetPath(copyPath), + customDefaultSdfAssetPath, + pxr::TfToken("Normal Map"), + pxr::TfToken("Normal"), + "", + pxr::TfToken("raw")); + if (!errorsMaterialNormalMapTexture.empty()) + { + errors.insert( + errors.end(), + errorsMaterialNormalMapTexture.begin(), + errorsMaterialNormalMapTexture.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the normal map of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + } + if (!pbrWorkflow->RoughnessMap().empty()) + { + std::string copyPath = + getMaterialCopyPath(pbrWorkflow->RoughnessMap()); + + std::string fullnameRoughnessMap = + ignition::common::findFile( + ignition::common::basename(pbrWorkflow->RoughnessMap())); + + if (fullnameRoughnessMap.empty()) + { + fullnameRoughnessMap = pbrWorkflow->RoughnessMap(); + } + + copyMaterial(copyPath, fullnameRoughnessMap); + + auto errorsMaterialReflectionRoughnessTexture = + CreateMaterialInput( + shaderPrim, + "reflectionroughness_texture", + pxr::SdfValueTypeNames->Asset, + pxr::SdfAssetPath(copyPath), + customDefaultSdfAssetPath, + pxr::TfToken("RoughnessMap Map"), + pxr::TfToken("RoughnessMap"), + "", + pxr::TfToken("raw")); + if (!errorsMaterialReflectionRoughnessTexture.empty()) + { + errors.insert( + errors.end(), + errorsMaterialReflectionRoughnessTexture.begin(), + errorsMaterialReflectionRoughnessTexture.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the roughness map of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + + const std::map + customDataRoughnessTextureInfluence = + { + {pxr::TfToken("default"), pxr::VtValue(0)}, + {pxr::TfToken("range:max"), pxr::VtValue(1)}, + {pxr::TfToken("range:min"), pxr::VtValue(0)} + }; + + auto errorsMaterialReflectionRoughnessTextureInfluence = + CreateMaterialInput( + shaderPrim, + "reflection_roughness_texture_influence", + pxr::SdfValueTypeNames->Bool, + true, + customDataRoughnessTextureInfluence, + pxr::TfToken("Roughness Map Influence"), + pxr::TfToken("Reflectivity"), + "", + pxr::TfToken("raw")); + if (!errorsMaterialReflectionRoughnessTextureInfluence.empty()) + { + errors.insert( + errors.end(), + errorsMaterialReflectionRoughnessTextureInfluence.begin(), + errorsMaterialReflectionRoughnessTextureInfluence.end()); + errors.push_back(UsdError(UsdErrorCode::INVALID_MATERIAL, + "Unable to set the reflectivity of the material at path [" + + _materialPath.GetString() + "]")); + return errors; + } + } + } + } + + return errors; + } +} +} +} diff --git a/usd/src/sdf_parser/Material_Sdf2Usd_TEST.cc b/usd/src/sdf_parser/Material_Sdf2Usd_TEST.cc new file mode 100644 index 000000000..657c5360e --- /dev/null +++ b/usd/src/sdf_parser/Material_Sdf2Usd_TEST.cc @@ -0,0 +1,243 @@ +/* + * Copyright 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include + +// TODO(ahcorde) this is to remove deprecated "warnings" in usd, these warnings +// are reported using #pragma message so normal diagnostic flags cannot remove +// them. This workaround requires this block to be used whenever usd is +// included. +#pragma push_macro ("__DEPRECATED") +#undef __DEPRECATED +#include +#include +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/usd/sdf_parser/World.hh" +#include "sdf/Root.hh" +#include "test_config.h" +#include "test_utils.hh" +#include "../UsdTestUtils.hh" + +void CheckMaterial( + const pxr::UsdPrim &_prim, + const pxr::GfVec3f &_diffuseColor, + const pxr::GfVec3f &_emissiveColor, + bool _hasPbr, + const std::string &_albedoName = "", + const std::string &_normalName = "", + const std::string &_roughnessName = "", + const std::string &_metallicName = "") +{ + auto variantshader = pxr::UsdShadeShader(_prim); + ASSERT_TRUE(variantshader); + + std::vector inputs = variantshader.GetInputs(); + pxr::GfVec3f diffuseColor {0, 0, 0}; + pxr::GfVec3f emissiveColor {0, 0, 0}; + + // PBR-specific values + const float _metallicConstant = 0.5; + const bool _enableEmission = true; + + bool checkedDiffuse = false; + bool checkedEmissive = false; + bool checkedAlbedo = false; + bool checkedNormal = false; + bool checkedRoughness = false; + bool checkedMetallicName = false; + bool checkedMetallicConstant = false; + bool checkedEnableEmission = false; + for (auto &input : inputs) + { + if (_hasPbr && input.GetBaseName() == "diffuse_texture") + { + pxr::SdfAssetPath materialPathUSD; + pxr::UsdShadeInput diffuseTextureShaderInput = + variantshader.GetInput(pxr::TfToken("diffuse_texture")); + diffuseTextureShaderInput.Get(&materialPathUSD); + EXPECT_EQ(_albedoName, + materialPathUSD.GetAssetPath()); + checkedAlbedo = true; + } + else if (_hasPbr && input.GetBaseName() == "metallic_constant") + { + pxr::UsdShadeInput metallicConstantShaderInput = + variantshader.GetInput(pxr::TfToken("metallic_constant")); + float metallicConstant; + metallicConstantShaderInput.Get(&metallicConstant); + EXPECT_FLOAT_EQ(_metallicConstant, metallicConstant); + checkedMetallicConstant = true; + } + else if (input.GetBaseName() == "enable_emission") + { + bool enableEmission; + pxr::UsdShadeInput enableEmissiveShaderInput = + variantshader.GetInput(pxr::TfToken("enable_emission")); + enableEmissiveShaderInput.Get(&enableEmission); + EXPECT_EQ(_enableEmission, enableEmission); + checkedEnableEmission = true; + } + else if (_hasPbr && input.GetBaseName() == "normalmap_texture") + { + pxr::SdfAssetPath materialPath; + pxr::UsdShadeInput normalTextureShaderInput = + variantshader.GetInput(pxr::TfToken("normalmap_texture")); + normalTextureShaderInput.Get(&materialPath); + EXPECT_EQ(_normalName, materialPath.GetAssetPath()); + checkedNormal = true; + } + else if (_hasPbr && input.GetBaseName() == "reflectionroughness_texture") + { + pxr::SdfAssetPath materialPath; + pxr::UsdShadeInput roughnessTextureShaderInput = + variantshader.GetInput(pxr::TfToken("reflectionroughness_texture")); + roughnessTextureShaderInput.Get(&materialPath); + EXPECT_EQ(_roughnessName, materialPath.GetAssetPath()); + checkedRoughness = true; + } + else if (_hasPbr && input.GetBaseName() == "metallic_texture") + { + pxr::SdfAssetPath materialPath; + pxr::UsdShadeInput metallicTextureShaderInput = + variantshader.GetInput(pxr::TfToken("metallic_texture")); + metallicTextureShaderInput.Get(&materialPath); + EXPECT_EQ(_metallicName, materialPath.GetAssetPath()); + checkedMetallicName = true; + } + else if (input.GetBaseName() == "emissive_color") + { + pxr::UsdShadeInput emissiveColorShaderInput = + variantshader.GetInput(pxr::TfToken("emissive_color")); + if (emissiveColorShaderInput.Get(&emissiveColor)) + { + EXPECT_EQ(_emissiveColor, emissiveColor); + } + checkedEmissive = true; + } + else if (input.GetBaseName() == "diffuse_color_constant") + { + auto sourceInfoV = input.GetConnectedSources(); + if (sourceInfoV.size() > 0) + { + pxr::UsdShadeInput connectedInput = + sourceInfoV[0].source.GetInput(sourceInfoV[0].sourceName); + + const pxr::SdfPath& thisAttrPath = connectedInput.GetAttr().GetPath(); + auto connectedPrim = _prim.GetStage()->GetPrimAtPath( + thisAttrPath.GetPrimPath()); + if (connectedPrim) + { + connectedPrim.GetAttribute( + pxr::TfToken("inputs:diffuse_color_constant")).Get(&diffuseColor); + } + } + else + { + pxr::UsdShadeInput diffuseShaderInput = + variantshader.GetInput(pxr::TfToken("diffuse_color_constant")); + diffuseShaderInput.Get(&diffuseColor); + } + EXPECT_EQ(_diffuseColor, diffuseColor); + checkedDiffuse = true; + } + } + EXPECT_TRUE(checkedDiffuse); + EXPECT_TRUE(checkedEmissive); + EXPECT_TRUE(checkedEnableEmission); + EXPECT_EQ(_hasPbr, checkedAlbedo); + EXPECT_EQ(_hasPbr, checkedNormal); + EXPECT_EQ(_hasPbr, checkedRoughness); + EXPECT_EQ(_hasPbr, checkedMetallicName); + EXPECT_EQ(_hasPbr, checkedMetallicConstant); +} + +///////////////////////////////////////////////// +// Fixture that creates a USD stage for each test case. +class UsdStageFixture : public::testing::Test +{ + public: UsdStageFixture() = default; + + protected: void SetUp() override + { + this->stage = pxr::UsdStage::CreateInMemory(); + ASSERT_TRUE(this->stage); + } + + public: pxr::UsdStageRefPtr stage; +}; + +///////////////////////////////////////////////// +TEST_F(UsdStageFixture, Material) +{ + sdf::setFindCallback(sdf::usd::testing::findFileCb); + ignition::common::addFindFileURICallback( + std::bind(&sdf::usd::testing::FindResourceUri, std::placeholders::_1)); + + const auto path = sdf::testing::TestFile("sdf", "basic_shapes.sdf"); + sdf::Root root; + + ASSERT_TRUE(sdf::testing::LoadSdfFile(path, root)); + ASSERT_EQ(1u, root.WorldCount()); + auto world = root.WorldByIndex(0u); + + const auto worldPath = std::string("/" + world->Name()); + auto usdErrors = sdf::usd::ParseSdfWorld(*world, stage, worldPath); + EXPECT_TRUE(usdErrors.empty()); + + auto worldPrim = this->stage->GetPrimAtPath(pxr::SdfPath(worldPath)); + ASSERT_TRUE(worldPrim); + + const std::string meshGeometryPath = worldPath + "/mesh/link/visual/geometry"; + ASSERT_TRUE(this->stage->GetPrimAtPath(pxr::SdfPath(meshGeometryPath))); + + { + const std::string materialPath = "/Looks/Material_2"; + ASSERT_TRUE(this->stage->GetPrimAtPath(pxr::SdfPath(materialPath))); + + const std::string materialshaderPath = materialPath + "/Shader"; + const auto materialShaderPrim = this->stage->GetPrimAtPath( + pxr::SdfPath(materialshaderPath)); + ASSERT_TRUE(materialShaderPrim); + + CheckMaterial(materialShaderPrim, + pxr::GfVec3f(0.2, 0.5, 0.1), + pxr::GfVec3f(0, 0, 0), + true, + "materials/textures/albedo_map.png", + "materials/textures/normal_map.png", + "materials/textures/roughness_map.png", + "materials/textures/metalness_map.png"); + } + + { + const std::string materialPath = "/Looks/Material_0"; + ASSERT_TRUE(this->stage->GetPrimAtPath(pxr::SdfPath(materialPath))); + + const std::string materialshaderPath = materialPath + "/Shader"; + const auto materialShaderPrim = this->stage->GetPrimAtPath( + pxr::SdfPath(materialshaderPath)); + ASSERT_TRUE(materialShaderPrim); + + CheckMaterial(materialShaderPrim, + pxr::GfVec3f(0, 0.1, 0.2), + pxr::GfVec3f(0.12, 0.23, 0.34), + false); + } +} diff --git a/usd/src/sdf_parser/Visual.cc b/usd/src/sdf_parser/Visual.cc index af2d52d6c..3dbbb889c 100644 --- a/usd/src/sdf_parser/Visual.cc +++ b/usd/src/sdf_parser/Visual.cc @@ -36,6 +36,7 @@ #include "sdf/Visual.hh" #include "sdf/usd/sdf_parser/Geometry.hh" +#include "sdf/usd/sdf_parser/Material.hh" #include "../UsdUtils.hh" namespace sdf @@ -90,6 +91,45 @@ namespace usd return errors; } + if (auto geomPrim = _stage->GetPrimAtPath(pxr::SdfPath(geometryPath))) + { + if (_visual.Material()) + { + pxr::SdfPath materialPath; + UsdErrors materialErrors = ParseSdfMaterial( + _visual.Material(), _stage, materialPath); + if (!materialErrors.empty()) + { + errors.insert(errors.end(), materialErrors.begin(), + materialErrors.end()); + errors.push_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Error parsing material attached to visual [" + + _visual.Name() + "]")); + return errors; + } + + auto materialUSD = + pxr::UsdShadeMaterial(_stage->GetPrimAtPath(materialPath)); + if (!materialUSD) + { + errors.push_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Unable to convert prim at path [" + materialPath.GetString() + + "] to a pxr::UsdShadeMaterial.")); + return errors; + } + pxr::UsdShadeMaterialBindingAPI(geomPrim).Bind(materialUSD); + } + } + else + { + errors.push_back(UsdError(sdf::usd::UsdErrorCode::INVALID_PRIM_PATH, + "Internal error: no geometry prim exists at path [" + + geometryPath + "]")); + return errors; + } + return errors; } }