diff --git a/.github/ci/before_cmake.sh b/.github/ci/before_cmake.sh index 123af216d..8172bf198 100644 --- a/.github/ci/before_cmake.sh +++ b/.github/ci/before_cmake.sh @@ -7,12 +7,17 @@ BUILD_DIR=`pwd` cd /tmp # check that we can compile USD from sources (only Focal) +# see https://github.com/ignitionrobotics/sdformat/issues/869 +return_code=0 +if [ "$(lsb_release -r -s)" != "20.04" ]; then + return_code=$(($return_code + 1)) +fi + mkdir cmake_test cd cmake_test echo "cmake_minimum_required(VERSION 3.12)" > CMakeLists.txt -return_code=0 cmake . || return_code=$(($return_code + $?)) if [ $return_code -eq 0 ] then diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26bd9f069..164a62cd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,3 +21,12 @@ jobs: codecov-enabled: true cppcheck-enabled: true cpplint-enabled: true + jammy-ci: + runs-on: ubuntu-latest + name: Ubuntu Jammy CI + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Compile and test + id: ci + uses: ignition-tooling/action-ignition-ci@jammy diff --git a/Changelog.md b/Changelog.md index 5d204f626..38176b613 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,123 @@ ## libsdformat 12.X +### libsdformat 12.4.0 (2022-03-xx) + +1. SDF -> USD: + * [Pull request #818](https://github.com/ignitionrobotics/sdformat/pull/818) + * [Pull request #828](https://github.com/ignitionrobotics/sdformat/pull/828) + * [Pull request #829](https://github.com/ignitionrobotics/sdformat/pull/829) + * [Pull request #830](https://github.com/ignitionrobotics/sdformat/pull/830) + * [Pull request #831](https://github.com/ignitionrobotics/sdformat/pull/831) + * [Pull request #837](https://github.com/ignitionrobotics/sdformat/pull/837) + +1. ToElement + * [Pull request #771](https://github.com/ignitionrobotics/sdformat/pull/771) + * [Pull request #772](https://github.com/ignitionrobotics/sdformat/pull/772) + * [Pull request #775](https://github.com/ignitionrobotics/sdformat/pull/775) + * [Pull request #776](https://github.com/ignitionrobotics/sdformat/pull/776) + * [Pull request #777](https://github.com/ignitionrobotics/sdformat/pull/777) + * [Pull request #781](https://github.com/ignitionrobotics/sdformat/pull/781) + * [Pull request #782](https://github.com/ignitionrobotics/sdformat/pull/782) + * [Pull request #783](https://github.com/ignitionrobotics/sdformat/pull/783) + +1. Fix compiler warnings + * [Pull request #808](https://github.com/ignitionrobotics/sdformat/pull/808) + * [Pull request #810](https://github.com/ignitionrobotics/sdformat/pull/810) + +1. Infrastructure and Documentation + * [Pull request #861](https://github.com/ignitionrobotics/sdformat/pull/861) + * [Pull request #800](https://github.com/ignitionrobotics/sdformat/pull/800) + * [Pull request #686](https://github.com/ignitionrobotics/sdformat/pull/686) + * [Pull request #713](https://github.com/ignitionrobotics/sdformat/pull/713) + +1. Use the Plugin DOM in other DOM objects + * [Pull request #858](https://github.com/ignitionrobotics/sdformat/pull/858) + +1. Add SDFormat tags for Triggered Cameras + * [Pull request #846](https://github.com/ignitionrobotics/sdformat/pull/846) + +1. Fix bug where //include/pose was ignored when using the Interface API + * [Pull request #853](https://github.com/ignitionrobotics/sdformat/pull/853) + +1. Fix joint parent/child frame existence checks to include interface elements + * [Pull request #855](https://github.com/ignitionrobotics/sdformat/pull/855) + +1. Remove USD visibility macro from internal APIs + * [Pull request #857](https://github.com/ignitionrobotics/sdformat/pull/857) + +1. Added non-const mutable accessors for world child objects + * [Pull request #840](https://github.com/ignitionrobotics/sdformat/pull/840) + +1. Added non-const accessors for Model child objects + * [Pull request #839](https://github.com/ignitionrobotics/sdformat/pull/839) + +1. Added to light if the light is on or off + * [Pull request #851](https://github.com/ignitionrobotics/sdformat/pull/851) + +1. Added Root mutable accessors, and Root::Clone function + * [Pull request #841](https://github.com/ignitionrobotics/sdformat/pull/841) + +1. Hide USDUtils.hh file from public API + * [Pull request #850](https://github.com/ignitionrobotics/sdformat/pull/850) + +1. Added non-const accessors for Link child objects + * [Pull request #838](https://github.com/ignitionrobotics/sdformat/pull/838) + +1. Add USDError class + * [Pull request #836](https://github.com/ignitionrobotics/sdformat/pull/836) + +1. Use USD component visibility macro + * [Pull request #849](https://github.com/ignitionrobotics/sdformat/pull/849) + +1. Add support for merge-include in the Interface API + * [Pull request #768](https://github.com/ignitionrobotics/sdformat/pull/768) + +1. Handle `__model__` in joint parent or child when using merge-include + * [Pull request #835](https://github.com/ignitionrobotics/sdformat/pull/835) + +1. Allow model frames (__model__) to be used as joint parent or child + * [Pull request #833](https://github.com/ignitionrobotics/sdformat/pull/833) + +1. Fix bug where a sdf::ParserConfig object was not passed to all sdf::readFile calls + * [Pull request #824](https://github.com/ignitionrobotics/sdformat/pull/824) + +1. Make SDF to USD a separate component of sdformat + * [Pull request #817](https://github.com/ignitionrobotics/sdformat/pull/817) + +1. Add ParserConfig flag for preserveFixedJoint + * [Pull request #815](https://github.com/ignitionrobotics/sdformat/pull/815) + +1. Fix parsing 'type' attibutes in plugins + * [Pull request #809](https://github.com/ignitionrobotics/sdformat/pull/809) + +1. sdf_custom: fix nested model expectations + * [Pull request #807](https://github.com/ignitionrobotics/sdformat/pull/807) + +1. Replace custom cmake code with ign-cmake2 + * [Pull request #780](https://github.com/ignitionrobotics/sdformat/pull/780) + +1. Support printing sdf poses in degrees and allow snapping to commonly used angles + * [Pull request #689](https://github.com/ignitionrobotics/sdformat/pull/689) + +1. Refactor FrameSemantics.cc + * [Pull request #764](https://github.com/ignitionrobotics/sdformat/pull/764) + +1. Fix loading nested include with custom attributes + * [Pull request #789](https://github.com/ignitionrobotics/sdformat/pull/789) + +1. Added plugin to SDF DOM + * [Pull request #788](https://github.com/ignitionrobotics/sdformat/pull/788) + +1. Support URI in the Model DOM + * [Pull request #786](https://github.com/ignitionrobotics/sdformat/pull/786) + +1. Support adding and clearing sensors from a joint + * [Pull request #785](https://github.com/ignitionrobotics/sdformat/pull/785) + +1. PrintConfig option to preserve includes when converting to string + * [Pull request #749](https://github.com/ignitionrobotics/sdformat/pull/749) + ### libsdformat 12.3.0 (2021-12-01) 1. Fix empty pose parsing fail for rotation_format='quat_xyzw' diff --git a/src/parser.cc b/src/parser.cc index b7cd3492b..3f946845a 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -155,7 +155,9 @@ static bool isSdfFile(const std::string &_fileName) ////////////////////////////////////////////////// template -static inline bool _initFile(const std::string &_filename, TPtr _sdf) +static inline bool _initFile(const std::string &_filename, + const ParserConfig &_config, + TPtr _sdf) { auto xmlDoc = makeSdfDoc(); if (tinyxml2::XML_SUCCESS != xmlDoc.LoadFile(_filename.c_str())) @@ -165,7 +167,7 @@ static inline bool _initFile(const std::string &_filename, TPtr _sdf) return false; } - return initDoc(&xmlDoc, _sdf); + return initDoc(&xmlDoc, _config, _sdf); } ////////////////////////////////////////////////// @@ -181,7 +183,9 @@ static inline bool _initFile(const std::string &_filename, TPtr _sdf) /// \param[out] _errors Captures errors encountered during parsing. static void insertIncludedElement(sdf::SDFPtr _includeSDF, const SourceLocation &_sourceLoc, bool _merge, - sdf::ElementPtr _parent, sdf::Errors &_errors) + sdf::ElementPtr _parent, + const ParserConfig &_config, + sdf::Errors &_errors) { Error invalidFileError(ErrorCode::FILE_READ, "Included model is invalid. Skipping model."); @@ -230,7 +234,7 @@ static void insertIncludedElement(sdf::SDFPtr _includeSDF, // We create a throwaway sdf::Root object in order to validate the // included entity. sdf::Root includedRoot; - sdf::Errors includeDOMerrors = includedRoot.Load(_includeSDF); + sdf::Errors includeDOMerrors = includedRoot.Load(_includeSDF, _config); _errors.insert(_errors.end(), includeDOMerrors.begin(), includeDOMerrors.end()); @@ -369,7 +373,7 @@ bool init(SDFPtr _sdf) std::string xmldata = SDF::EmbeddedSpec("root.sdf", false); auto xmlDoc = makeSdfDoc(); xmlDoc.Parse(xmldata.c_str()); - return initDoc(&xmlDoc, _sdf); + return initDoc(&xmlDoc, ParserConfig::GlobalConfig(), _sdf); } ////////////////////////////////////////////////// @@ -387,9 +391,10 @@ bool initFile( { auto xmlDoc = makeSdfDoc(); xmlDoc.Parse(xmldata.c_str()); - return initDoc(&xmlDoc, _sdf); + return initDoc(&xmlDoc, _config, _sdf); } - return _initFile(sdf::findFile(_filename, true, false, _config), _sdf); + return _initFile(sdf::findFile(_filename, true, false, _config), _config, + _sdf); } ////////////////////////////////////////////////// @@ -407,14 +412,15 @@ bool initFile( { auto xmlDoc = makeSdfDoc(); xmlDoc.Parse(xmldata.c_str()); - return initDoc(&xmlDoc, _sdf); + return initDoc(&xmlDoc, _config, _sdf); } - return _initFile(sdf::findFile(_filename, true, false, _config), _sdf); + return _initFile(sdf::findFile(_filename, true, false, _config), _config, + _sdf); } ////////////////////////////////////////////////// bool initString( - const std::string &_xmlString, const ParserConfig &, SDFPtr _sdf) + const std::string &_xmlString, const ParserConfig &_config, SDFPtr _sdf) { auto xmlDoc = makeSdfDoc(); if (xmlDoc.Parse(_xmlString.c_str())) @@ -423,7 +429,7 @@ bool initString( return false; } - return initDoc(&xmlDoc, _sdf); + return initDoc(&xmlDoc, _config, _sdf); } ////////////////////////////////////////////////// @@ -452,7 +458,9 @@ inline tinyxml2::XMLElement *_initDocGetElement(tinyxml2::XMLDocument *_xmlDoc) } ////////////////////////////////////////////////// -bool initDoc(tinyxml2::XMLDocument *_xmlDoc, SDFPtr _sdf) +bool initDoc(tinyxml2::XMLDocument *_xmlDoc, + const ParserConfig &_config, + SDFPtr _sdf) { auto element = _initDocGetElement(_xmlDoc); if (!element) @@ -460,11 +468,13 @@ bool initDoc(tinyxml2::XMLDocument *_xmlDoc, SDFPtr _sdf) return false; } - return initXml(element, _sdf->Root()); + return initXml(element, _config, _sdf->Root()); } ////////////////////////////////////////////////// -bool initDoc(tinyxml2::XMLDocument *_xmlDoc, ElementPtr _sdf) +bool initDoc(tinyxml2::XMLDocument *_xmlDoc, + const ParserConfig &_config, + ElementPtr _sdf) { auto element = _initDocGetElement(_xmlDoc); if (!element) @@ -472,11 +482,13 @@ bool initDoc(tinyxml2::XMLDocument *_xmlDoc, ElementPtr _sdf) return false; } - return initXml(element, _sdf); + return initXml(element, _config, _sdf); } ////////////////////////////////////////////////// -bool initXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf) +bool initXml(tinyxml2::XMLElement *_xml, + const ParserConfig &_config, + ElementPtr _sdf) { const char *refString = _xml->Attribute("ref"); if (refString) @@ -594,7 +606,7 @@ bool initXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf) else { ElementPtr element(new Element); - initXml(child, element); + initXml(child, _config, element); _sdf->AddElementDescription(element); } } @@ -607,7 +619,7 @@ bool initXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf) ElementPtr element(new Element); - initFile(filename, element); + initFile(filename, _config, element); // override description for include elements tinyxml2::XMLElement *description = child->FirstChildElement("description"); @@ -1510,7 +1522,7 @@ bool readXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf, ElementPtr refSDF; refSDF.reset(new Element); std::string refFilename = refSDFStr + ".sdf"; - initFile(refFilename, refSDF); + initFile(refFilename, _config, refSDF); _sdf->RemoveFromParent(); _sdf->Copy(refSDF); @@ -1803,7 +1815,8 @@ bool readXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf, SourceLocation sourceLoc{includeXmlPath, _source, elemXml->GetLineNum()}; - insertIncludedElement(includeSDF, sourceLoc, toMerge, _sdf, _errors); + insertIncludedElement(includeSDF, sourceLoc, toMerge, _sdf, _config, + _errors); continue; } } diff --git a/src/parser_private.hh b/src/parser_private.hh index c1b8d963f..70a5a5c5d 100644 --- a/src/parser_private.hh +++ b/src/parser_private.hh @@ -46,16 +46,24 @@ namespace sdf /// This actually forwards to initXml after converting the inputs /// \param[in] _xmlDoc TinyXML2 document containing the SDFormat description /// file that corresponds with the input SDFPtr + /// \param[in] _config Custom parser configuration /// \param[out] _sdf SDF interface to be initialized - bool initDoc(tinyxml2::XMLDocument *_xmlDoc, SDFPtr _sdf); + /// \return True on success, false on error. + bool initDoc(tinyxml2::XMLDocument *_xmlDoc, + const ParserConfig &_config, + SDFPtr _sdf); /// \brief Initialize the SDF Element using a TinyXML2 document /// /// This actually forwards to initXml after converting the inputs /// \param[in] _xmlDoc TinyXML2 document containing the SDFormat description /// file that corresponds with the input ElementPtr + /// \param[in] _config Custom parser configuration /// \param[out] _sdf SDF Element to be initialized - bool initDoc(tinyxml2::XMLDocument *_xmlDoc, ElementPtr _sdf); + /// \return True on success, false on error. + bool initDoc(tinyxml2::XMLDocument *_xmlDoc, + const ParserConfig &_config, + ElementPtr _sdf); /// \brief Initialize the SDF Element by parsing the SDFormat description in /// the input TinyXML2 element. This is where SDFormat spec/description files @@ -63,8 +71,12 @@ namespace sdf /// \remark For internal use only. Do not use this function. /// \param[in] _xml TinyXML2 element containing the SDFormat description /// file that corresponds with the input ElementPtr + /// \param[in] _config Custom parser configuration /// \param[out] _sdf SDF ElementPtr to be initialized - bool initXml(tinyxml2::XMLElement *_xml, ElementPtr _sdf); + /// \return True on success, false on error. + bool initXml(tinyxml2::XMLElement *_xml, + const ParserConfig &_config, + ElementPtr _sdf); /// \brief Populate the SDF values from a TinyXML document bool readDoc(tinyxml2::XMLDocument *_xmlDoc, SDFPtr _sdf, @@ -83,6 +95,7 @@ namespace sdf /// \param[in] _xmlRoot Pointer to the root level TinyXML element. /// \param[in] _source Source of the XML document. /// \param[in] _errors Captures errors found during the checks. + /// \return True on success, false on error. bool checkXmlFromRoot(tinyxml2::XMLElement *_xmlRoot, const std::string &_source, Errors &_errors); diff --git a/test/usd/materials/textures/FANS_Albedo.png b/test/usd/materials/textures/FANS_Albedo.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/usd/materials/textures/FANS_Metalness.png b/test/usd/materials/textures/FANS_Metalness.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/usd/materials/textures/FANS_Normal.png b/test/usd/materials/textures/FANS_Normal.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/usd/materials/textures/FANS_Roughness.png b/test/usd/materials/textures/FANS_Roughness.png new file mode 100644 index 000000000..e69de29bb diff --git a/test/usd/upAxisY.usda b/test/usd/upAxisY.usda new file mode 100644 index 000000000..000ea5db7 --- /dev/null +++ b/test/usd/upAxisY.usda @@ -0,0 +1,198 @@ +#usda 1.0 +( + metersPerUnit = 1.0 + upAxis = "Y" +) + +def "shapes" +{ + def Xform "ground_plane" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, -0.125) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "link" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Cube "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + rel material:binding = + double size = 1 + float3 xformOp:scale = (100, 100, 0.25) + uniform token[] xformOpOrder = ["xformOp:scale"] + } + } + } + } + + def Xform "box" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "box_link" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0, 0, 0) + float3 physics:diagonalInertia = (0.16666, 0.16666, 0.16666) + float physics:mass = 1 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "box_visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Cube "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + rel material:binding = + double size = 1 + float3 xformOp:scale = (1, 1, 1) + uniform token[] xformOpOrder = ["xformOp:scale"] + } + } + } + } + + def Scope "Looks" + { + def Material "Material_0" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (0.8, 0.8, 0.8) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_1" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (1, 0, 0) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + } +} diff --git a/test/usd/upAxisZ.usda b/test/usd/upAxisZ.usda new file mode 100644 index 000000000..6933533c8 --- /dev/null +++ b/test/usd/upAxisZ.usda @@ -0,0 +1,721 @@ +#usda 1.0 +( + metersPerUnit = 0.01 + upAxis = "Z" +) + + +def "shapes" +{ + def PhysicsScene "physics" + { + vector3f physics:gravityDirection = (0, 0, -1) + float physics:gravityMagnitude = 9.8 + } + + def Xform "ground_plane" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, -0.125) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "link" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Cube "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + rel material:binding = + double size = 1 + float3 xformOp:scale = (100, 100, 0.25) + uniform token[] xformOpOrder = ["xformOp:scale"] + } + } + } + } + + def Xform "box" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "box_link" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0, 0, 0) + float3 physics:diagonalInertia = (0.16666, 0.16666, 0.16666) + float physics:mass = 1 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "box_visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Cube "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + rel material:binding = + double size = 1 + float3 xformOp:scale = (1, 1, 1) + uniform token[] xformOpOrder = ["xformOp:scale"] + } + } + } + } + + def Xform "cylinder" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, -1.5, 0.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "cylinder_link" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0, 0, 0) + float3 physics:diagonalInertia = (0.1458, 0.1458, 0.125) + float physics:mass = 1 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "cylinder_visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Cylinder "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + double height = 1 + rel material:binding = + double radius = 0.5 + } + } + } + } + + def Xform "sphere" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 1.5, 0.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "sphere_link" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0, 0, 0) + float3 physics:diagonalInertia = (0.1, 0.1, 0.1) + float physics:mass = 1 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "sphere_visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Sphere "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + rel material:binding = + double radius = 0.5 + } + } + } + } + + def Xform "capsule" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, -3, 0.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "capsule_link" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0, 0, 0) + float3 physics:diagonalInertia = (0.074154, 0.074154, 0.018769) + float physics:mass = 1 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "capsule_visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Capsule "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.2, -0.2, -0.5), (0.2, 0.2, 0.5)] + double height = 0.6 + rel material:binding = + double radius = 0.2 + } + } + } + } + + def Xform "ellipsoid" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 3, 0.5) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "ellipsoid_link" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsMassAPI"] + ) + { + point3f physics:centerOfMass = (0, 0, 0) + float3 physics:diagonalInertia = (0.068, 0.058, 0.026) + float physics:mass = 1 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Xform "ellipsoid_visual" + { + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + + def Sphere "geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + float3[] extent = [(-0.5, -0.5, -0.5), (0.5, 0.5, 0.5)] + rel material:binding = + double radius = 0.5 + float3 xformOp:scale = (0.4, 0.6, 1) + uniform token[] xformOpOrder = ["xformOp:scale"] + } + } + } + } + + def DistantLight "sun" + { + float inputs:intensity = 1000 + float intensity = 100 + float3 xformOp:rotateXYZ = (0, 0, 0) + double3 xformOp:translate = (0, 0, 10) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + } +} + +def Scope "Looks" +{ + def Material "Material_0" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (0.8, 0.8, 0.8) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_1" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (1, 0, 0) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_2" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (0, 1, 0) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_3" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (0, 0, 1) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_4" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (1, 1, 0) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_5" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (1, 0, 1) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + token outputs:out + } + } + + def Material "Material_textures" + { + token outputs:mdl:displacement.connect = + token outputs:mdl:surface.connect = + token outputs:mdl:volume.connect = + + def Shader "Shader" + { + uniform token info:implementationSource = "sourceAsset" + uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@ + uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR" + color3f inputs:diffuse_color_constant = (1, 1, 1) ( + customData = { + float3 default = (0.2, 0.2, 0.2) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Albedo" + displayName = "Base Color" + doc = "This is the base color" + ) + asset inputs:diffuse_texture = @materials/textures/FANS_Albedo.png@ ( + colorSpace = "auto" + customData = { + asset default = @@ + } + displayGroup = "Albedo" + displayName = "Base Map" + ) + color3f inputs:emissive_color = (0, 0, 0) ( + customData = { + float3 default = (1, 0.1, 0.1) + dictionary range = { + float3 max = (100000, 100000, 100000) + float3 min = (0, 0, 0) + } + } + displayGroup = "Emissive" + displayName = "Emissive Color" + doc = "The emission color" + ) + float inputs:emissive_intensity = 1 ( + customData = { + int default = 40 + dictionary range = { + int max = 100000 + int min = 0 + } + } + displayGroup = "Emissive" + displayName = "Emissive Intensity" + doc = "Intensity of the emission" + ) + bool inputs:enable_emission = 1 ( + customData = { + int default = 0 + } + displayGroup = "Emissive" + displayName = "Enable Emissive" + doc = "Enables the emission of light from the material" + ) + float inputs:metallic_constant = 0.5 ( + customData = { + double default = 0.5 + dictionary range = { + int max = 1 + int min = 0 + } + } + displayGroup = "Reflectivity" + displayName = "Metallic Amount" + doc = "Metallic Material" + ) + asset inputs:metallic_texture = @materials/textures/FANS_Metalness.png@ ( + colorSpace = "raw" + customData = { + asset default = @@ + } + displayGroup = "Reflectivity" + displayName = "Metallic Map" + ) + asset inputs:normalmap_texture = @materials/textures/FANS_Normal.png@ ( + colorSpace = "raw" + customData = { + asset default = @@ + } + displayGroup = "Normal" + displayName = "Normal Map" + ) + float inputs:reflection_roughness_constant = 0.5 ( + customData = { + double default = 0.5 + dictionary range = { + int max = 1 + int min = 0 + } + } + displayGroup = "Reflectivity" + displayName = "Roughness Amount" + doc = "Higher roughness values lead to more blurry reflections" + ) + bool inputs:reflection_roughness_texture_influence = 1 ( + colorSpace = "raw" + customData = { + int default = 0 + dictionary range = { + int max = 1 + int min = 0 + } + } + displayGroup = "Reflectivity" + displayName = "Roughness Map Influence" + ) + asset inputs:reflectionroughness_texture = @materials/textures/FANS_Roughness.png@ ( + colorSpace = "raw" + customData = { + asset default = @@ + } + displayGroup = "RoughnessMap" + displayName = "RoughnessMap Map" + ) + token outputs:out + } + } +} diff --git a/test/usd/upAxis_wrong.usda b/test/usd/upAxis_wrong.usda new file mode 100644 index 000000000..f35d392d9 --- /dev/null +++ b/test/usd/upAxis_wrong.usda @@ -0,0 +1,5 @@ +#usda 1.0 +( + metersPerUnit = 0.009999999776482582 + upAxis = "X" +) diff --git a/usd/include/sdf/usd/UsdError.hh b/usd/include/sdf/usd/UsdError.hh index 28742e2d6..b83c8ae5f 100644 --- a/usd/include/sdf/usd/UsdError.hh +++ b/usd/include/sdf/usd/UsdError.hh @@ -69,6 +69,18 @@ namespace sdf /// \brief Invalid material INVALID_MATERIAL, + + /// \brief Invalid usd file + INVALID_USD_FILE, + + /// \brief Invalid up axis + INVALID_UP_AXIS, + + /// \brief Prim is missing a particular attribute + PRIM_MISSING_ATTRIBUTE, + + /// \brief Prim is of the incorrect schema type + PRIM_INCORRECT_SCHEMA_TYPE, }; class IGNITION_SDFORMAT_USD_VISIBLE UsdError diff --git a/usd/include/sdf/usd/usd_parser/Parser.hh b/usd/include/sdf/usd/usd_parser/Parser.hh new file mode 100644 index 000000000..6f638cb99 --- /dev/null +++ b/usd/include/sdf/usd/usd_parser/Parser.hh @@ -0,0 +1,46 @@ +/* + * 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_USD_PARSER_PARSER_HH +#define SDF_USD_USD_PARSER_PARSER_HH + +#include + +#include "sdf/config.hh" +#include "sdf/usd/Export.hh" +#include "sdf/usd/UsdError.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief Parse a USD file and convert it to a SDF file + /// \param[in] _inputFilenameUsd Path of the USD file to parse + /// \param[in] _outputFilenameSdf Path where the SDF file will be located + /// \return UsdErrors, which is a vector of UsdError objects. Each UsdError + /// includes an error code and message. An empty vector indicates no error + /// occurred when parsing the USD file to its SDF representation. + UsdErrors IGNITION_SDFORMAT_USD_VISIBLE parseUSDFile( + const std::string &_inputFilenameUsd, + const std::string &_outputFilenameSdf); + } + } +} +#endif diff --git a/usd/include/sdf/usd/usd_parser/USDData.hh b/usd/include/sdf/usd/usd_parser/USDData.hh new file mode 100644 index 000000000..d76e861dd --- /dev/null +++ b/usd/include/sdf/usd/usd_parser/USDData.hh @@ -0,0 +1,118 @@ +/* + * 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_USD_PARSER_USDDATA_HH_ +#define SDF_USD_USD_PARSER_USDDATA_HH_ + +#include +#include +#include +#include +#include + +#include + +#include "sdf/Material.hh" +#include "sdf/Types.hh" +#include "sdf/sdf_config.h" +#include "sdf/system_util.hh" +#include "sdf/usd/usd_parser/USDStage.hh" +#include "sdf/usd/Export.hh" +#include "sdf/usd/UsdError.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief this class will handle the data of a stage + /// It will parse the materials in the stage + /// If the stage has some references to other stages, this class + /// will read them and make this data available here too. + class IGNITION_SDFORMAT_USD_VISIBLE USDData + { + /// \brief Constructor + public: explicit USDData(const std::string &_filename); + + /// \brief Initialize the data inside the class with the stage + /// defined in the constructor + /// \return A vector of Error objects. Each Error includes + /// an error code and message. An empty vector indicates no error. + public: UsdErrors Init(); + + /// \brief If a stage contains substages, this will allow to include + /// them. + /// \return A vector of Error objects. Each Error includes + /// an error code and message. An empty vector indicates no error. + public: UsdErrors AddStage(const std::string &_ref); + + /// \brief Read materials + /// \return A vector of Error objects. Each Error includes + /// an error code and message. An empty vector indicates no error. + public: UsdErrors ParseMaterials(); + + /// \brief Get all materials readed in the stage + public: const std::unordered_map & + Materials() const; + + /// \brief Get all sdformat models inside the stage + public: const std::set &Models() const; + + /// \brief Get all stages (the main one and all the referenced). + public: const std::unordered_map> & + AllReferences() const; + + /// \brief Find a path and get the data + /// \param[in] _name Name of the path to find + /// \return A pair with the name of the stage and the data + public: const std::pair> + FindStage(const std::string &_name); + + public: friend std::ostream& operator<<( + std::ostream& os, const USDData& data) + { + os << "References:" << "\n"; + for (auto &ref : data.AllReferences()) + { + os << "\t" << ref.first << "\n"; + os << "\t\t" << ref.second->UpAxis() << "\n"; + os << "\t\t" << ref.second->MetersPerUnit() << "\n"; + for (auto &path : ref.second->USDPaths()) + { + os << "\t\tPath " << path << '\n'; + } + } + os << "Models:" << "\n"; + auto models = data.Models(); + for (const auto &model : models) + { + os << "\t" << model << "\n"; + } + + return os; + } + + /// \brief Private data pointer. + IGN_UTILS_IMPL_PTR(dataPtr) + }; + } +} +} + +#endif diff --git a/usd/include/sdf/usd/usd_parser/USDStage.hh b/usd/include/sdf/usd/usd_parser/USDStage.hh new file mode 100644 index 000000000..2bc8f5d25 --- /dev/null +++ b/usd/include/sdf/usd/usd_parser/USDStage.hh @@ -0,0 +1,69 @@ +/* + * 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_USD_PARSER_USDSTAGE_HH_ +#define SDF_USD_USD_PARSER_USDSTAGE_HH_ + +#include +#include + +#include + +#include "sdf/Types.hh" +#include "sdf/sdf_config.h" +#include "sdf/system_util.hh" +#include "sdf/usd/Export.hh" +#include "sdf/usd/UsdError.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief this class will keep the data of a stage + /// - UpAxis + /// - MetersPerUnit + /// - All USD paths + class IGNITION_SDFORMAT_USD_VISIBLE USDStage + { + /// \brief Default constructor + /// \param[in] _refFileName File name of the stage in the disk + public: explicit USDStage(const std::string &_refFileName); + + /// \brief Initialize the data structure + /// \return A vector of Error objects. Each Error includes + /// an error code and message. An empty vector indicates no error. + public: UsdErrors Init(); + + /// \brief Get stage up axis + public: const std::string &UpAxis() const; + + /// \brief Get stage meter per unit + public: double MetersPerUnit() const; + + /// \brief Get USD paths available in the stage + public: const std::set &USDPaths() const; + + /// \brief Private data pointer. + IGN_UTILS_IMPL_PTR(dataPtr) + }; + } + } +} +#endif // SDF_USD_PARSER_USDSTAGE_HH diff --git a/usd/src/CMakeLists.txt b/usd/src/CMakeLists.txt index 29b4ef4b5..8067cd929 100644 --- a/usd/src/CMakeLists.txt +++ b/usd/src/CMakeLists.txt @@ -10,6 +10,13 @@ set(sources sdf_parser/Sensor.cc sdf_parser/Visual.cc sdf_parser/World.cc + usd_parser/Parser.cc + usd_parser/USD2SDF.cc + usd_parser/USDData.cc + usd_parser/USDMaterial.cc + usd_parser/USDPhysics.cc + usd_parser/USDStage.cc + usd_parser/USDWorld.cc ) ign_add_component(usd SOURCES ${sources} GET_TARGET_NAME usd_target) @@ -28,16 +35,18 @@ target_link_libraries(${usd_target} set(gtest_sources sdf_parser/sdf2usd_TEST.cc + usd_parser/usd2sdf_TEST.cc sdf_parser/Geometry_Sdf2Usd_TEST.cc 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 + usd_parser/USDData_TEST.cc + usd_parser/USDPhysics_TEST.cc + usd_parser/USDStage_TEST.cc Conversions_TEST.cc UsdError_TEST.cc UsdUtils_TEST.cc @@ -51,4 +60,9 @@ ign_build_tests( INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/test ) +if (TARGET UNIT_USDPhysics_TEST) + target_sources(UNIT_USDPhysics_TEST PRIVATE + usd_parser/USDPhysics.cc) +endif() + add_subdirectory(cmd) diff --git a/usd/src/cmd/CMakeLists.txt b/usd/src/cmd/CMakeLists.txt index 46b6ca8a3..2f480353a 100644 --- a/usd/src/cmd/CMakeLists.txt +++ b/usd/src/cmd/CMakeLists.txt @@ -9,9 +9,20 @@ if(TARGET ${usd_target}) ${usd_target} ) + add_executable(usd2sdf + usd2sdf.cc + ) + + target_link_libraries(usd2sdf + PUBLIC + ignition-utils${IGN_UTILS_VER}::ignition-utils${IGN_UTILS_VER} + ${usd_target} + ) + install( TARGETS sdf2usd + usd2sdf DESTINATION ${BIN_INSTALL_DIR} ) diff --git a/usd/src/cmd/usd2sdf.cc b/usd/src/cmd/usd2sdf.cc new file mode 100644 index 000000000..43dd7b794 --- /dev/null +++ b/usd/src/cmd/usd2sdf.cc @@ -0,0 +1,93 @@ +/* + * 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 +#include + +#include + +#include "sdf/usd/usd_parser/Parser.hh" +#include "sdf/config.hh" + +////////////////////////////////////////////////// +/// \brief Enumeration of available commands +enum class Command +{ + kNone, +}; + +////////////////////////////////////////////////// +/// \brief Structure to hold all filenames +struct Options +{ + /// \brief Command to execute + Command command{Command::kNone}; + + /// \brief input filename + std::string inputFilename{"input.usd"}; + + /// \brief output filename + std::string outputFilename{"output.sdf"}; +}; + +void runCommand(const Options &_opt) +{ + const auto errors = + sdf::usd::parseUSDFile(_opt.inputFilename, _opt.outputFilename); + if (!errors.empty()) + { + std::cerr << "Errors occurred when generating [" << _opt.outputFilename + << "] from [" << _opt.inputFilename << "]:\n"; + for (const auto &e : errors) + std::cerr << "\t" << e << "\n"; + } +} + +void addFlags(CLI::App &_app) +{ + auto opt = std::make_shared(); + + _app.add_option("input", + opt->inputFilename, + "Input filename. Defaults to input.usd unless otherwise specified."); + + _app.add_option("output", + opt->outputFilename, + "Output filename. Defaults to output.sdf unless otherwise specified."); + + _app.callback([&_app, opt](){ + runCommand(*opt); + }); +} + +////////////////////////////////////////////////// +int main(int argc, char** argv) +{ + CLI::App app{"USD to SDF converter"}; + + app.set_help_all_flag("--help-all", "Show all help"); + + app.add_flag_callback("--version", [](){ + std::cout << SDF_VERSION_FULL << std::endl; + throw CLI::Success(); + }); + + addFlags(app); + CLI11_PARSE(app, argc, argv); + + return 0; +} diff --git a/usd/src/usd_parser/Parser.cc b/usd/src/usd_parser/Parser.cc new file mode 100644 index 000000000..3fe584744 --- /dev/null +++ b/usd/src/usd_parser/Parser.cc @@ -0,0 +1,55 @@ +/* + * 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/usd_parser/Parser.hh" +#include "USD2SDF.hh" + +#include "sdf/Root.hh" + +namespace sdf +{ +inline namespace SDF_VERSION_NAMESPACE { +namespace usd +{ + UsdErrors parseUSDFile( + const std::string &_inputFilenameUsd, + const std::string &_outputFilenameSdf) + { + UsdErrors errors; + USD2SDF usd2sdf; + sdf::Root root; + errors = usd2sdf.Read(_inputFilenameUsd, root); + if (!errors.empty()) + { + return errors; + } + + std::ofstream out(_outputFilenameSdf.c_str(), std::ios::out); + if (!out) + { + errors.emplace_back(UsdError( + UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Unable to open file [" + _outputFilenameSdf + "] for writing")); + return errors; + } + out << root.ToElement()->ToString(""); + out.close(); + return errors; + } +} +} +} diff --git a/usd/src/usd_parser/USD2SDF.cc b/usd/src/usd_parser/USD2SDF.cc new file mode 100644 index 000000000..ef68c19f5 --- /dev/null +++ b/usd/src/usd_parser/USD2SDF.cc @@ -0,0 +1,66 @@ +/* + * 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 "USD2SDF.hh" + +#include "sdf/Console.hh" +#include "sdf/Types.hh" +#include "USDWorld.hh" + +#include "sdf/Error.hh" +#include "sdf/Root.hh" +#include "sdf/World.hh" + +#include "sdf/usd/UsdError.hh" + +namespace sdf { +inline namespace SDF_VERSION_NAMESPACE { +namespace usd { +//////////////////////////////////////////////// +UsdErrors USD2SDF::Read(const std::string &_fileName, + sdf::Root &_root) +{ + UsdErrors errors; + + sdf::World sdfWorld; + + errors = parseUSDWorld(_fileName, sdfWorld); + if (!errors.empty()) + { + errors.emplace_back(UsdError( + UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Error parsing usd file [" + _fileName + "]")); + return errors; + } + + const auto addWorldErrors = _root.AddWorld(sdfWorld); + if (!addWorldErrors.empty()) + { + for (const auto & error : addWorldErrors) + { + errors.emplace_back(error); + } + errors.emplace_back(UsdError(sdf::Error( + sdf::ErrorCode::ELEMENT_INVALID, + "Error adding the world [" + sdfWorld.Name() + "] to the SDF DOM"))); + } + + return errors; +} +} +} +} diff --git a/usd/src/usd_parser/USD2SDF.hh b/usd/src/usd_parser/USD2SDF.hh new file mode 100644 index 000000000..9844ff043 --- /dev/null +++ b/usd/src/usd_parser/USD2SDF.hh @@ -0,0 +1,56 @@ +/* + * 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 USD_PARSER_USD2SDF_HH_ +#define USD_PARSER_USD2SDF_HH_ + +#include + +#include "sdf/sdf_config.h" +#include "sdf/usd/UsdError.hh" + +#include "sdf/Root.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { + namespace usd + { + /// \brief USD to SDF converter + /// This class helps generate the SDF file + class USD2SDF + { + /// \brief constructor + public: USD2SDF() = default; + + /// \brief convert USD file to a sdf::Root object + /// \param[in] _fileName string containing USD filename. + /// \param[out] _root Root element to populate with the equivalent sdf + /// information from _fileName. + /// \return UsdErrors, which is a list of UsdError objects. An empty list + /// means no errors occurred when populating _root with the contents + /// of _fileName + public: UsdErrors Read( + const std::string &_fileName, + sdf::Root &_root); + }; + } +} +} + +#endif diff --git a/usd/src/usd_parser/USDData.cc b/usd/src/usd_parser/USDData.cc new file mode 100644 index 000000000..535b24c44 --- /dev/null +++ b/usd/src/usd_parser/USDData.cc @@ -0,0 +1,309 @@ +/* + * 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/usd_parser/USDData.hh" + +#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 +#pragma pop_macro ("__DEPRECATED") + +#include +#include + +#include "sdf/Material.hh" +#include "USDMaterial.hh" + +namespace sdf { +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +// +namespace usd { + /// \brief USDStage private data. + class USDData::Implementation + { + public: + /// File name of the main stage + std::string filename; + + /// Directory where the main stage is located + std::string directoryPath; + + /// map with the filename and its own Stage data + std::unordered_map> references; + + /// Models + std::set models; + + /// Materials availables in the stage and substages + std::unordered_map materials; + + /// Add all subdirectories that are inside the stage folder + /// This will help us to find other stages and/or textures. + /// \param[in] _path Path of the subdirectory to add + void AddSubdirectories(const std::string &_path) + { + 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); + this->AddSubdirectories(current); + } + } + } + }; + + ///////////////////////////////////////////////// + USDData::USDData(const std::string &_filename) + : dataPtr(ignition::utils::MakeImpl()) + { + this->dataPtr->filename = _filename; + } + + ///////////////////////////////////////////////// + const std::unordered_map & + USDData::Materials() const + { + return this->dataPtr->materials; + } + + ///////////////////////////////////////////////// + const std::set &USDData::Models() const + { + return this->dataPtr->models; + } + + ///////////////////////////////////////////////// + const std::unordered_map> & + USDData::AllReferences() const + { + return this->dataPtr->references; + } + + ///////////////////////////////////////////////// + UsdErrors USDData::Init() + { + UsdErrors errors; + + auto usdStage = std::make_shared(this->dataPtr->filename); + UsdErrors errorsInit = usdStage->Init(); + if(!errorsInit.empty()) + { + errors.insert(errors.end(), errorsInit.begin(), errorsInit.end()); + return errors; + } + + // Add the base stage to the structure + this->dataPtr->references.insert( + {this->dataPtr->filename, + usdStage + }); + + // it's the stage available + auto referencee = pxr::UsdStage::Open(this->dataPtr->filename); + if (!referencee) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::INVALID_USD_FILE, + "Failed to load usd file")); + return errors; + } + + this->dataPtr->directoryPath = ignition::common::absPath( + this->dataPtr->filename); + + std::string basename = ignition::common::basename( + this->dataPtr->directoryPath); + this->dataPtr->directoryPath = ignition::common::replaceAll( + this->dataPtr->directoryPath, basename, ""); + + this->dataPtr->AddSubdirectories(this->dataPtr->directoryPath); + + auto range = pxr::UsdPrimRange::Stage(referencee); + + for (auto const &_prim : range) + { + if (_prim.IsA()) + { + continue; + } + + if (_prim.IsA()) + { + continue; + } + + std::string primName = pxr::TfStringify(_prim.GetPath()); + std::vector tokens = ignition::common::split(primName, "/"); + if (tokens.size() == 1) + { + this->dataPtr->models.insert(tokens[0]); + } + + pxr::UsdPrimCompositionQuery query = + pxr::UsdPrimCompositionQuery::GetDirectReferences(_prim); + + std::vector arcs = + query.GetCompositionArcs(); + for (auto & a : arcs ) + { + pxr::SdfLayerHandle handler = a.GetIntroducingLayer(); + + for (auto & ref : handler->GetCompositionAssetDependencies()) + { + this->AddStage(ref); + } + } + } + + return errors; + } + + ///////////////////////////////////////////////// + UsdErrors USDData::ParseMaterials() + { + UsdErrors errors; + auto referencee = pxr::UsdStage::Open(this->dataPtr->filename); + if (!referencee) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::INVALID_USD_FILE, + "Failed to load usd file")); + return errors; + } + + auto range = pxr::UsdPrimRange::Stage(referencee); + + for (auto const &prim : range) + { + std::string primName = pxr::TfStringify(prim.GetPath()); + if (prim.IsA()) + { + std::string materialName = prim.GetName(); + + if (this->dataPtr->materials.find(materialName) + != this->dataPtr->materials.end()) + { + continue; + } + + sdf::Material material; + UsdErrors errrosMaterial = ParseMaterial(prim, material); + if (!errrosMaterial.empty()) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Error parsing material")); + errors.insert( + errors.end(), errrosMaterial.begin(), errrosMaterial.end()); + return errors; + } + this->dataPtr->materials.insert(std::pair( + materialName, material)); + } + } + return errors; + } + + ///////////////////////////////////////////////// + const std::pair> + USDData::FindStage(const std::string &_name) + { + for (auto &ref : this->dataPtr->references) + { + if (ref.second != nullptr) + { + for (auto &path : ref.second->USDPaths()) + { + if (path == _name) + { + return ref; + } + } + } + } + return std::make_pair("", nullptr); + } + + ///////////////////////////////////////////////// + UsdErrors USDData::AddStage(const std::string &_ref) + { + UsdErrors errors; + std::string key = _ref; + + auto search = this->dataPtr->references.find(_ref); + if (search == this->dataPtr->references.end()) + { + std::string basename = ignition::common::basename(key); + auto subDirectory = ignition::common::replaceAll(key, basename, ""); + + auto systemPaths = ignition::common::systemPaths(); + systemPaths->AddFilePaths(ignition::common::joinPaths( + this->dataPtr->directoryPath, subDirectory)); + + this->dataPtr->AddSubdirectories(ignition::common::joinPaths( + this->dataPtr->directoryPath, subDirectory)); + + std::string fileNameRef = ignition::common::findFile(basename); + if (fileNameRef.empty()) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Not able to find asset [" + _ref + "]")); + return errors; + } + + auto usdStage = std::make_shared(fileNameRef); + UsdErrors errorsInit = usdStage->Init(); + if(!errorsInit.empty()) + { + errors.insert(errors.end(), errorsInit.begin(), errorsInit.end()); + return errors; + } + + this->dataPtr->references.insert( + {key, + std::make_shared(fileNameRef) + }); + return errors; + } + else + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::SDF_TO_USD_PARSING_ERROR, + "Element already exists")); + return errors; + } + } +} +} +} diff --git a/usd/src/usd_parser/USDData_TEST.cc b/usd/src/usd_parser/USDData_TEST.cc new file mode 100644 index 000000000..a245b2075 --- /dev/null +++ b/usd/src/usd_parser/USDData_TEST.cc @@ -0,0 +1,123 @@ +/* + * 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 + +#include +#include + +#include + +#include +#include + +#include "test_config.h" +#include "test_utils.hh" + +#include +#include + +///////////////////////////////////////////////// +TEST(USDData, Constructor) +{ + // Open a invalid USD file + sdf::usd::USDData data(sdf::testing::TestFile("usd", "invalid_name")); + sdf::usd::UsdErrors errors = data.Init(); + EXPECT_EQ(1u, errors.size()); + + // Add test/usd directory to find some resources + auto systemPaths = ignition::common::systemPaths(); + systemPaths->AddFilePaths(sdf::testing::TestFile("usd")); + + // Open a valid USD file + { + sdf::testing::ScopeExit removeCopiedMaterials( + [] + { + ignition::common::removeAll( + ignition::common::joinPaths(ignition::common::cwd(), "materials")); + }); + + std::string filename = sdf::testing::TestFile("usd", "upAxisZ.usda"); + sdf::usd::USDData usdData(filename); + EXPECT_EQ(0u, usdData.Init().size()); + EXPECT_EQ(0u, usdData.ParseMaterials().size()); + + EXPECT_EQ(1u, usdData.AllReferences().size()); + EXPECT_EQ(2u, usdData.Models().size()); + EXPECT_EQ(7u, usdData.Materials().size()); + + auto materials = usdData.Materials(); + + auto material0 = materials["Material_0"]; + EXPECT_EQ(ignition::math::Color(0.8, 0.8, 0.8), material0.Diffuse()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), material0.Emissive()); + + auto material1 = materials["Material_1"]; + EXPECT_EQ(ignition::math::Color(1, 0, 0), material1.Diffuse()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), material1.Emissive()); + + auto material2 = materials["Material_2"]; + EXPECT_EQ(ignition::math::Color(0, 1, 0), material2.Diffuse()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), material2.Emissive()); + + auto material3 = materials["Material_3"]; + EXPECT_EQ(ignition::math::Color(0, 0, 1), material3.Diffuse()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), material3.Emissive()); + + auto material4 = materials["Material_4"]; + EXPECT_EQ(ignition::math::Color(1, 1, 0), material4.Diffuse()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), material4.Emissive()); + + auto material5 = materials["Material_5"]; + EXPECT_EQ(ignition::math::Color(1, 0, 1), material5.Diffuse()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), material5.Emissive()); + + auto materialTextures = materials["Material_textures"]; + const auto * pbr = materialTextures.PbrMaterial(); + ASSERT_TRUE(pbr); + const auto * workflow = pbr->Workflow(sdf::PbrWorkflowType::METAL); + ASSERT_TRUE(workflow); + EXPECT_EQ(ignition::math::Color(1, 1, 1), materialTextures.Diffuse()); + EXPECT_EQ( + "materials/textures/FANS_Albedo.png", + workflow->AlbedoMap()); + EXPECT_EQ(ignition::math::Color(0, 0, 0), materialTextures.Emissive()); + EXPECT_DOUBLE_EQ(0.5, workflow->Metalness()); + EXPECT_EQ( + "materials/textures/FANS_Metalness.png", + workflow->MetalnessMap()); + EXPECT_DOUBLE_EQ(0.5, workflow->Roughness()); + EXPECT_EQ( + "materials/textures/FANS_Roughness.png", + workflow->RoughnessMap()); + EXPECT_EQ( + "materials/textures/FANS_Normal.png", + workflow->NormalMap()); + + // Find a path inside the stage + auto boxStage = usdData.FindStage("box"); + EXPECT_EQ(filename, boxStage.first); + EXPECT_EQ("Z", boxStage.second->UpAxis()); + EXPECT_DOUBLE_EQ(0.01, boxStage.second->MetersPerUnit()); + + // Try to find a invalid path in the stage data + auto invalidStage = usdData.FindStage("invalid"); + EXPECT_EQ("", invalidStage.first); + EXPECT_EQ(nullptr, invalidStage.second); + } +} diff --git a/usd/src/usd_parser/USDMaterial.cc b/usd/src/usd_parser/USDMaterial.cc new file mode 100644 index 000000000..9a63b34f2 --- /dev/null +++ b/usd/src/usd_parser/USDMaterial.cc @@ -0,0 +1,326 @@ +/* + * 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 +#include "USDMaterial.hh" + +#include +#include +#include +#include +#include + +#include "sdf/Pbr.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + ///////////////////////////////////////////////// + /// \brief Copy a file from one destination to another + /// \param[in] _ori The original file to copy + /// \param[in] _dest The destination for the copy of _ori + /// \return A list of UsdErrors. An empty list means no errors occurred when + /// copying _ori to _dest + UsdErrors copyFile(const std::string &_ori, const std::string &_dest) + { + UsdErrors errors; + if (ignition::common::exists(_ori)) + { + std::string baseName = ignition::common::basename(_dest); + std::string pathDest = ignition::common::replaceAll(_dest, baseName, ""); + ignition::common::createDirectories(pathDest); + if (!ignition::common::copyFile(_ori, _dest)) + { + errors.emplace_back( + Error(ErrorCode::FILE_READ, "Unable to copy the file [" + _ori + + "] to [" + _dest + "]")); + } + } + else + { + errors.emplace_back( + Error(ErrorCode::FILE_READ, "File [" + _ori + "] does not exist")); + } + return errors; + } + + /// \brief Helper method for getting an asset path + /// \param[in] _tokenName Name of the asset + /// \param[in] _shader Shader that holds the desired asset + /// \return The pxr::SdfAssetPath object that contains the asset identified by + /// _tokenName in _shader + pxr::SdfAssetPath assetPath(const pxr::TfToken &_tokenName, + const pxr::UsdShadeShader &_shader) + { + pxr::SdfAssetPath assetPath; + pxr::UsdShadeInput shadeInput = _shader.GetInput(_tokenName); + shadeInput.Get(&assetPath); + return assetPath; + } + + ///////////////////////////////////////////////// + UsdErrors ParseMaterial(const pxr::UsdPrim &_prim, sdf::Material &_material) + { + UsdErrors errors; + // if the prim is a Geom then get the color values + if (_prim.IsA()) + { + auto variantGeom = pxr::UsdGeomGprim(_prim); + + pxr::VtArray color(0, 0, 0); + variantGeom.GetDisplayColorAttr().Get(&color); + + pxr::VtFloatArray displayOpacity; + const std::string displayOpacityToken = "primvars:displayOpacity"; + auto opacityAttr = _prim.GetAttribute(pxr::TfToken(displayOpacityToken)); + if (!opacityAttr) + { + errors.push_back(UsdError(UsdErrorCode::PRIM_MISSING_ATTRIBUTE, + "Prim at path [" + _prim.GetPath().GetString() + + "] does not have an attribute with a pxr::TfToken of [" + + displayOpacityToken + "]")); + return errors; + } + opacityAttr.Get(&displayOpacity); + + double alpha = 1.0; + if (displayOpacity.size() > 0) + { + alpha = 1 - displayOpacity[0]; + } + // Refer to this comment in github to understand the ambient and diffuse + // https://github.com/osrf/sdformat/pull/526#discussion_r623937715 + _material.SetAmbient( + ignition::math::Color( + ignition::math::clamp(color[0][2] / 0.4, 0.0, 1.0), + ignition::math::clamp(color[0][1] / 0.4, 0.0, 1.0), + ignition::math::clamp(color[0][0] / 0.4, 0.0, 1.0), + alpha)); + _material.SetDiffuse( + ignition::math::Color( + ignition::math::clamp(color[0][2] / 0.8, 0.0, 1.0), + ignition::math::clamp(color[0][1] / 0.8, 0.0, 1.0), + ignition::math::clamp(color[0][0] / 0.8, 0.0, 1.0), + alpha)); + } + // if the prim is a shade Material then get the texture values + else if (_prim.IsA()) + { + auto variantMaterial = pxr::UsdShadeMaterial(_prim); + for (const auto &child : _prim.GetChildren()) + { + if (child.IsA()) + { + auto variantShader = pxr::UsdShadeShader(child); + + bool enableEmission = false; + bool isPBR = false; + sdf::PbrWorkflow pbrWorkflow; + ignition::math::Color emissiveColorCommon; + + for (const auto &input : variantShader.GetInputs()) + { + if (input.GetBaseName() == "diffuse_texture") + { + pxr::SdfAssetPath materialPath = + assetPath(pxr::TfToken("diffuse_texture"), variantShader); + pbrWorkflow.SetAlbedoMap(materialPath.GetAssetPath()); + std::string fullAlbedoName = + ignition::common::findFile(materialPath.GetAssetPath()); + UsdErrors errorCopy = copyFile( + fullAlbedoName, materialPath.GetAssetPath()); + if (!errorCopy.empty()) + { + errors.insert(errors.end(), errorCopy.begin(), errorCopy.end()); + return errors; + } + isPBR = true; + } + else if (input.GetBaseName() == "normalmap_texture") + { + pxr::SdfAssetPath materialPath = + assetPath(pxr::TfToken("normalmap_texture"), variantShader); + pbrWorkflow.SetNormalMap(materialPath.GetAssetPath()); + std::string fullNormalName = + ignition::common::findFile(materialPath.GetAssetPath()); + auto errorCopy = copyFile(fullNormalName, + materialPath.GetAssetPath()); + if (!errorCopy.empty()) + { + errors.insert(errors.end(), errorCopy.begin(), errorCopy.end()); + return errors; + } + isPBR = true; + } + else if (input.GetBaseName() == "reflectionroughness_texture") + { + pxr::SdfAssetPath materialPath = assetPath( + pxr::TfToken("reflectionroughness_texture"), variantShader); + pbrWorkflow.SetRoughnessMap(materialPath.GetAssetPath()); + std::string fullRoughnessName = + ignition::common::findFile(materialPath.GetAssetPath()); + auto errorCopy = copyFile( + fullRoughnessName, materialPath.GetAssetPath()); + if (!errorCopy.empty()) + { + errors.insert(errors.end(), errorCopy.begin(), errorCopy.end()); + return errors; + } + isPBR = true; + } + else if (input.GetBaseName() == "metallic_texture") + { + pxr::SdfAssetPath materialPath = assetPath( + pxr::TfToken("metallic_texture"), variantShader); + pbrWorkflow.SetMetalnessMap(materialPath.GetAssetPath()); + std::string fullMetalnessName = + ignition::common::findFile(materialPath.GetAssetPath()); + auto errorCopy = copyFile( + fullMetalnessName, materialPath.GetAssetPath()); + if (!errorCopy.empty()) + { + errors.insert(errors.end(), errorCopy.begin(), errorCopy.end()); + return errors; + } + isPBR = true; + } + else if (input.GetBaseName() == "diffuse_color_constant") + { + pxr::GfVec3f diffuseColor(0, 0, 0); + 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); + } + _material.SetDiffuse( + ignition::math::Color( + diffuseColor[0], + diffuseColor[1], + diffuseColor[2])); + } + else if (input.GetBaseName() == "metallic_constant") + { + pxr::UsdShadeInput metallicConstantShaderInput = + variantShader.GetInput(pxr::TfToken("metallic_constant")); + float metallicConstant; + metallicConstantShaderInput.Get(&metallicConstant); + pbrWorkflow.SetMetalness(metallicConstant); + isPBR = true; + } + else if (input.GetBaseName() == "reflection_roughness_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) + { + float reflectionRoughnessConstant; + connectedPrim.GetAttribute( + pxr::TfToken("inputs:reflection_roughness_constant")). + Get(&reflectionRoughnessConstant); + pbrWorkflow.SetRoughness(reflectionRoughnessConstant); + isPBR = true; + } + } + else + { + pxr::UsdShadeInput reflectionRoughnessConstantShaderInput = + variantShader.GetInput( + pxr::TfToken("reflection_roughness_constant")); + float reflectionRoughnessConstant; + reflectionRoughnessConstantShaderInput. + Get(&reflectionRoughnessConstant); + pbrWorkflow.SetRoughness(reflectionRoughnessConstant); + isPBR = true; + } + } + else if (input.GetBaseName() == "enable_emission") + { + pxr::UsdShadeInput enableEmissiveShaderInput = + variantShader.GetInput(pxr::TfToken("enable_emission")); + enableEmissiveShaderInput.Get(&enableEmission); + } + else if (input.GetBaseName() == "emissive_color") + { + pxr::GfVec3f emissiveColor(0, 0, 0); + pxr::UsdShadeInput emissiveColorShaderInput = + variantShader.GetInput(pxr::TfToken("emissive_color")); + if (emissiveColorShaderInput.Get(&emissiveColor)) + { + emissiveColorCommon = ignition::math::Color( + emissiveColor[0], + emissiveColor[1], + emissiveColor[2]); + } + } + } + + if (enableEmission) + { + _material.SetEmissive(emissiveColorCommon); + } + + if (isPBR) + { + sdf::Pbr pbr; + pbr.SetWorkflow(sdf::PbrWorkflowType::METAL, pbrWorkflow); + _material.SetPbrMaterial(pbr); + } + } + } + } + else + { + errors.push_back(UsdError(UsdErrorCode::PRIM_INCORRECT_SCHEMA_TYPE, + "Prim at path [" + _prim.GetPath().GetString() + + "is not a pxr::UsdGeomGprim or a pxr::UsdShadeMaterial.")); + } + + return errors; + } +} +} +} diff --git a/usd/src/usd_parser/USDMaterial.hh b/usd/src/usd_parser/USDMaterial.hh new file mode 100644 index 000000000..694727147 --- /dev/null +++ b/usd/src/usd_parser/USDMaterial.hh @@ -0,0 +1,54 @@ +/* + * 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_USD_PARSER_UTILS_HH_ +#define SDF_USD_USD_PARSER_UTILS_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 +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/Material.hh" +#include "sdf/config.hh" +#include "sdf/usd/UsdError.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// brief Parse the material in a USD prim to a sdf::Material + /// If the prim is a pxr::UsdGeomGprim, get the color values. Otherwise, + /// if the prim is a pxr::UsdShadeMaterial, get the texture values + /// \param[in] _prim USD prim where the material is extracted + /// \param[out] _material The sdf::Material representation of _prim's + /// material + /// \return UsdErrors, which is a vector of UsdError objects. Each UsdError + /// includes an error code and message. An empty vector indicates no error. + UsdErrors ParseMaterial(const pxr::UsdPrim &_prim, + sdf::Material &_material); +} +} +} +#endif diff --git a/usd/src/usd_parser/USDPhysics.cc b/usd/src/usd_parser/USDPhysics.cc new file mode 100644 index 000000000..3ef402843 --- /dev/null +++ b/usd/src/usd_parser/USDPhysics.cc @@ -0,0 +1,68 @@ +/* + * 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 "USDPhysics.hh" + +#pragma push_macro ("__DEPRECATED") +#undef __DEPRECATED +#include +#include +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/World.hh" + +namespace sdf +{ +inline namespace SDF_VERSION_NAMESPACE { +namespace usd +{ + void ParseUSDPhysicsScene( + const pxr::UsdPhysicsScene &_scene, + sdf::World &_world, + double _metersPerUnit) + { + ignition::math::Vector3d worldGravity{0, 0, -1}; + float magnitude {9.8f}; + const auto gravityAttr = _scene.GetGravityDirectionAttr(); + if (gravityAttr) + { + pxr::GfVec3f gravity; + gravityAttr.Get(&gravity); + if (!ignition::math::equal(0.0f, gravity[0]) && + !ignition::math::equal(0.0f, gravity[1]) && + !ignition::math::equal(0.0f, gravity[2])) + { + worldGravity[0] = gravity[0]; + worldGravity[1] = gravity[1]; + worldGravity[2] = gravity[2]; + } + } + + const auto magnitudeAttr = _scene.GetGravityMagnitudeAttr(); + if (magnitudeAttr) + { + magnitudeAttr.Get(&magnitude); + if (!std::isnan(magnitude) && !std::isinf(magnitude)) + { + magnitude = magnitude * _metersPerUnit; + } + } + _world.SetGravity(worldGravity * magnitude); + } +} +} +} diff --git a/usd/src/usd_parser/USDPhysics.hh b/usd/src/usd_parser/USDPhysics.hh new file mode 100644 index 000000000..b5835f011 --- /dev/null +++ b/usd/src/usd_parser/USDPhysics.hh @@ -0,0 +1,49 @@ +/* + * 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 USD_PARSER_PHYSYCS_HH +#define USD_PARSER_PHYSYCS_HH + +#pragma push_macro ("__DEPRECATED") +#undef __DEPRECATED +#include +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/config.hh" +#include "sdf/usd/Export.hh" + +#include "sdf/World.hh" +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief Parse the physics attributes of a USD file + /// \param[in] _scene USD physics scene to extract attributes from + /// \param[out] _world World interface where the data is placed + /// \param[in] _metersPerUnit meters per unit in the USD + void ParseUSDPhysicsScene( + const pxr::UsdPhysicsScene &_scene, + sdf::World &_world, + double _metersPerUnit); + } + } +} + +#endif diff --git a/usd/src/usd_parser/USDPhysics_TEST.cc b/usd/src/usd_parser/USDPhysics_TEST.cc new file mode 100644 index 000000000..9197383f7 --- /dev/null +++ b/usd/src/usd_parser/USDPhysics_TEST.cc @@ -0,0 +1,76 @@ +/* + * 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 + +#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 +#pragma pop_macro ("__DEPRECATED") + +#include "test_config.h" +#include "test_utils.hh" + +#include "sdf/World.hh" +#include "USDPhysics.hh" + +///////////////////////////////////////////////// +TEST(USDPhysicsTest, AvailablePhysics) +{ + const std::string filename = sdf::testing::TestFile("usd", "upAxisZ.usda"); + const auto stage = pxr::UsdStage::Open(filename); + ASSERT_TRUE(stage); + + const auto physicsScene = + pxr::UsdPhysicsScene(stage->GetPrimAtPath(pxr::SdfPath("/shapes/physics"))); + EXPECT_TRUE(physicsScene); + + sdf::World world; + + const double metersPerUnit = 1.0; + + sdf::usd::ParseUSDPhysicsScene( + physicsScene, world, metersPerUnit); + EXPECT_EQ(ignition::math::Vector3d(0, 0, -9.8), world.Gravity()); +} + +///////////////////////////////////////////////// +TEST(USDPhysicsTest, UnavailablePhysics) +{ + const std::string filename = sdf::testing::TestFile("usd", "upAxisY.usda"); + const auto stage = pxr::UsdStage::Open(filename); + ASSERT_TRUE(stage); + + const auto physicsScene = + pxr::UsdPhysicsScene(stage->GetPrimAtPath(pxr::SdfPath("/shapes/physics"))); + EXPECT_FALSE(physicsScene); + + sdf::World world; + + const double metersPerUnit = 1.0; + + sdf::usd::ParseUSDPhysicsScene( + physicsScene, world, metersPerUnit); + EXPECT_EQ(ignition::math::Vector3d(0, 0, -9.8), world.Gravity()); +} diff --git a/usd/src/usd_parser/USDStage.cc b/usd/src/usd_parser/USDStage.cc new file mode 100644 index 000000000..3524e6f7d --- /dev/null +++ b/usd/src/usd_parser/USDStage.cc @@ -0,0 +1,139 @@ +/* + * 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 +#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 +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/usd/usd_parser/USDStage.hh" + +namespace sdf { +// Inline bracke to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +// +namespace usd +{ + /// \brief USDStage private data. + class USDStage::Implementation + { + public: + /// \brief Up Axis this must be "Z" or "Y". + std::string upAxis = "Z"; + + /// \brief Meter per unit + double metersPerUnit = 1.0; + + /// \brief All USD paths available in the file + std::set paths; + + std::string refFileName; + }; + + ///////////////////////////////////////////////// + USDStage::USDStage(const std::string &_refFileName) + : dataPtr(ignition::utils::MakeImpl()) + { + this->dataPtr->refFileName = _refFileName; + } + + ///////////////////////////////////////////////// + UsdErrors USDStage::Init() + { + UsdErrors errors; + + // Open the stage + auto referencee = pxr::UsdStage::Open(this->dataPtr->refFileName); + if (!referencee) + { + errors.emplace_back(UsdError( + sdf::usd::UsdErrorCode::INVALID_USD_FILE, + "Failed to load usd file")); + return errors; + } + + // Get meters per unit + this->dataPtr->metersPerUnit = + pxr::UsdGeomGetStageMetersPerUnit(referencee); + + // is it up axis define, if so, read the value, if the value is + // not 'Y' or 'Z' throw an exception + if (referencee->HasAuthoredMetadata(pxr::UsdGeomTokens->upAxis)) + { + pxr::TfToken axis; + referencee->GetMetadata(pxr::UsdGeomTokens->upAxis, &axis); + this->dataPtr->upAxis = axis.GetText(); + + if (this->dataPtr->upAxis != "Y" && this->dataPtr->upAxis != "Z") + { + errors.emplace_back( + UsdError( + sdf::usd::UsdErrorCode::INVALID_UP_AXIS, + "Up axis should be 'Y' or 'Z'")); + return errors; + } + } + + // Keep all the USD paths + auto range = pxr::UsdPrimRange::Stage(referencee); + for (auto const &_prim : range) + { + if (_prim.IsA()) + { + continue; + } + if (_prim.IsA()) + { + continue; + } + + this->dataPtr->paths.insert(_prim.GetPath().GetName()); + } + return errors; + } + + ///////////////////////////////////////////////// + const std::string &USDStage::UpAxis() const + { + return this->dataPtr->upAxis; + } + + ///////////////////////////////////////////////// + double USDStage::MetersPerUnit() const + { + return this->dataPtr->metersPerUnit; + } + + ///////////////////////////////////////////////// + const std::set &USDStage::USDPaths() const + { + return this->dataPtr->paths; + } +} // usd +} // sdf version namespace +} // sdf diff --git a/usd/src/usd_parser/USDStage_TEST.cc b/usd/src/usd_parser/USDStage_TEST.cc new file mode 100644 index 000000000..246e8fb34 --- /dev/null +++ b/usd/src/usd_parser/USDStage_TEST.cc @@ -0,0 +1,71 @@ +/* + * 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 + +#include + +#include + +#include +#include + +#include "test_config.h" +#include "test_utils.hh" + +///////////////////////////////////////////////// +TEST(USDStage, Constructor) +{ + // Check up Axis equal to Z and metersPerUnit + { + std::string filename = sdf::testing::TestFile("usd", "upAxisZ.usda"); + sdf::usd::USDStage stage(filename); + sdf::usd::UsdErrors errors = stage.Init(); + EXPECT_EQ(0u, errors.size()); + + EXPECT_EQ("Z", stage.UpAxis()); + EXPECT_DOUBLE_EQ(0.01, stage.MetersPerUnit()); + EXPECT_EQ(23u, stage.USDPaths().size()); + } + + // Check up Axis equal to Y and metersPerUnit + { + std::string filename = sdf::testing::TestFile("usd", "upAxisY.usda"); + sdf::usd::USDStage stage(filename); + sdf::usd::UsdErrors errors = stage.Init(); + EXPECT_EQ(0u, errors.size()); + + EXPECT_EQ("Y", stage.UpAxis()); + EXPECT_DOUBLE_EQ(1.0, stage.MetersPerUnit()); + EXPECT_EQ(9u, stage.USDPaths().size()); + } + + // Wrong upaxis + { + sdf::usd::USDStage stage( + sdf::testing::TestFile("usd", "/upAxis_wrong.usda")); + sdf::usd::UsdErrors errors = stage.Init(); + EXPECT_EQ(1u, errors.size()); + } + + // Invalid file + { + sdf::usd::USDStage stage(sdf::testing::TestFile("usd", "invalid_name")); + sdf::usd::UsdErrors errors = stage.Init(); + EXPECT_EQ(1u, errors.size()); + } +} diff --git a/usd/src/usd_parser/USDWorld.cc b/usd/src/usd_parser/USDWorld.cc new file mode 100644 index 000000000..5c1ee6b03 --- /dev/null +++ b/usd/src/usd_parser/USDWorld.cc @@ -0,0 +1,91 @@ +/* + * 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 "USDWorld.hh" + +#include +#include +#include + +#pragma push_macro ("__DEPRECATED") +#undef __DEPRECATED +#include +#include +#pragma pop_macro ("__DEPRECATED") + +#include "sdf/usd/usd_parser/USDData.hh" +#include "sdf/usd/usd_parser/USDStage.hh" +#include "USDPhysics.hh" + +#include "sdf/World.hh" + +namespace sdf +{ +inline namespace SDF_VERSION_NAMESPACE { +namespace usd +{ + UsdErrors parseUSDWorld(const std::string &_inputFileName, + sdf::World &_world) + { + UsdErrors errors; + USDData usdData(_inputFileName); + usdData.Init(); + usdData.ParseMaterials(); + + auto reference = pxr::UsdStage::Open(_inputFileName); + if (!reference) + { + errors.emplace_back(UsdErrorCode::INVALID_USD_FILE, + "Unable to open [" + _inputFileName + "]"); + return errors; + } + std::string worldName = reference->GetDefaultPrim().GetName().GetText(); + if (worldName.empty()) + { + _world.SetName("world_name"); + } + else + { + _world.SetName(worldName + "_world"); + } + + auto range = pxr::UsdPrimRange::Stage(reference); + for (auto const &prim : range) + { + std::string primName = prim.GetName(); + + if (prim.IsA()) + { + std::pair> data = + usdData.FindStage(primName); + if (!data.second) + { + errors.push_back(UsdError(UsdErrorCode::INVALID_PRIM_PATH, + "Unable to retrieve the pxr::UsdPhysicsScene named [" + + primName + "]")); + return errors; + } + + ParseUSDPhysicsScene(pxr::UsdPhysicsScene(prim), _world, + data.second->MetersPerUnit()); + continue; + } + } + return errors; + } +} +} +} diff --git a/usd/src/usd_parser/USDWorld.hh b/usd/src/usd_parser/USDWorld.hh new file mode 100644 index 000000000..6574f501e --- /dev/null +++ b/usd/src/usd_parser/USDWorld.hh @@ -0,0 +1,47 @@ +/* + * 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 USD_PARSER_USDWORLD_HH +#define USD_PARSER_USDWORLD_HH + +#include + +#include "sdf/sdf_config.h" +#include "sdf/usd/UsdError.hh" + +#include "sdf/World.hh" + +namespace sdf +{ + // Inline bracket to help doxygen filtering. + inline namespace SDF_VERSION_NAMESPACE { + // + namespace usd + { + /// \brief Parse the world information of a USD file + /// \param[in] _inputFileNameUsd Path where the USD is located + /// \param[out] _world World interface where all USD world data is placed + /// \return UsdErrors, which is a vector of UsdError objects. Each UsdError + /// includes an error code and message. An empty vector indicates no error + /// occurred when parsing the world information of _inputFileNameUsd + UsdErrors parseUSDWorld( + const std::string &_inputFileNameUsd, + sdf::World &_world); + } + } +} +#endif diff --git a/usd/src/usd_parser/usd2sdf_TEST.cc b/usd/src/usd_parser/usd2sdf_TEST.cc new file mode 100644 index 000000000..fc73df3a1 --- /dev/null +++ b/usd/src/usd_parser/usd2sdf_TEST.cc @@ -0,0 +1,105 @@ +/* + * 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 + +#include + +#include +#include + +#include + +#include "sdf/Root.hh" +#include "sdf/World.hh" +#include "test_config.h" +#include "test_utils.hh" + +#ifdef _WIN32 + #define popen _popen + #define pclose _pclose +#endif + +static std::string usd2sdfCommand() +{ + return ignition::common::joinPaths(std::string(PROJECT_BINARY_DIR), "bin", + "usd2sdf"); +} + +///////////////////////////////////////////////// +std::string custom_exec_str(std::string _cmd) +{ + _cmd += " 2>&1"; + FILE *pipe = popen(_cmd.c_str(), "r"); + + if (!pipe) + return "ERROR"; + + char buffer[128]; + std::string result = ""; + + while (!feof(pipe)) + { + if (fgets(buffer, 128, pipe) != NULL) + result += buffer; + } + + pclose(pipe); + return result; +} + +///////////////////////////////////////////////// +TEST(version_cmd, IGN_UTILS_TEST_DISABLED_ON_WIN32(SDF)) +{ + std::string output = + custom_exec_str(usd2sdfCommand() + " --version"); + EXPECT_EQ(output, std::string(SDF_VERSION_FULL) + "\n"); +} + +///////////////////////////////////////////////// +TEST(check_cmd, IGN_UTILS_TEST_DISABLED_ON_WIN32(SDF)) +{ + const auto tmp = ignition::common::createTempDirectory("usd", + ignition::common::tempDirectoryPath()); + // Check a good SDF file + { + const std::string path = sdf::testing::TestFile("usd", "upAxisZ.usda"); + const auto outputSdfFilePath = + ignition::common::joinPaths(tmp, "upAxisZ.sdf"); + EXPECT_FALSE(ignition::common::isFile(outputSdfFilePath)); + const std::string output = + custom_exec_str(usd2sdfCommand() + " " + path + " " + outputSdfFilePath); + + // make sure that a sdf file was generated + ASSERT_TRUE(ignition::common::isFile(outputSdfFilePath)) << output; + + // check the contents of the generated SDF file + sdf::Root root; + const auto errors = root.Load(outputSdfFilePath); + EXPECT_TRUE(errors.empty()); + + // check the value of the gravity element + ASSERT_EQ(1u, root.WorldCount()); + const auto world = root.WorldByIndex(0u); + ASSERT_NE(nullptr, world); + EXPECT_DOUBLE_EQ(0.0, world->Gravity()[0]); + EXPECT_DOUBLE_EQ(0.0, world->Gravity()[1]); + EXPECT_DOUBLE_EQ(-0.098, world->Gravity()[2]); + + // TODO(anyone) Check the remaining contents of outputUsdFilePath + // when the parser is implemented + } +}