diff --git a/.github/ci/packages.apt b/.github/ci/packages.apt index 6d3f9a812..00715fab4 100644 --- a/.github/ci/packages.apt +++ b/.github/ci/packages.apt @@ -1,3 +1,4 @@ +libassimp-dev libavcodec-dev libavdevice-dev libavformat-dev diff --git a/CMakeLists.txt b/CMakeLists.txt index 112e2fb75..9c231c53c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,6 +122,10 @@ gz_find_package(AVCODEC REQUIRED_BY av PRETTY libavcodec) # Find avutil gz_find_package(AVUTIL REQUIRED_BY av PRETTY libavutil) +#------------------------------------ +# Find assimp +gz_find_package(GzAssimp REQUIRED_BY graphics PRETTY assimp) + message(STATUS "-------------------------------------------\n") diff --git a/graphics/include/gz/common/AssimpLoader.hh b/graphics/include/gz/common/AssimpLoader.hh new file mode 100644 index 000000000..f3267622a --- /dev/null +++ b/graphics/include/gz/common/AssimpLoader.hh @@ -0,0 +1,51 @@ +/* + * 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 GZ_COMMON_ASSIMPLOADER_HH_ +#define GZ_COMMON_ASSIMPLOADER_HH_ + +#include +#include +#include + +#include + +namespace gz +{ + namespace common + { + /// \class AssimpLoader AssimpLoader.hh gz/common/AssimpLoader.hh + /// \brief Class used to load mesh files using the assimp lodaer + class GZ_COMMON_GRAPHICS_VISIBLE AssimpLoader : public MeshLoader + { + /// \brief Constructor + public: AssimpLoader(); + + /// \brief Destructor + public: virtual ~AssimpLoader(); + + /// \brief Load a mesh + /// \param[in] _filename Mesh file to load + /// \return Pointer to a new Mesh + public: virtual Mesh *Load(const std::string &_filename) override; + + /// \internal + /// \brief Pointer to private data. + GZ_UTILS_UNIQUE_IMPL_PTR(dataPtr) + }; + } +} +#endif diff --git a/graphics/include/gz/common/Material.hh b/graphics/include/gz/common/Material.hh index f7929cd38..e90e793f1 100644 --- a/graphics/include/gz/common/Material.hh +++ b/graphics/include/gz/common/Material.hh @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -102,8 +103,11 @@ namespace gz /// \brief Set a texture image /// \param[in] _tex The name of the texture, which must be in the - /// resource path - public: void SetTextureImage(const std::string &_tex); + /// resource path or its name if _img is provided + /// \param[in] _img The image containing the texture if image has been + /// loaded in memory + public: void SetTextureImage(const std::string &_tex, + const std::shared_ptr &_img = nullptr); /// \brief Set a texture image /// \param[in] _tex The name of the texture @@ -111,6 +115,10 @@ namespace gz public: void SetTextureImage(const std::string &_tex, const std::string &_resourcePath); + /// \brief Gets the texture image, if the texture was loaded from memory + /// \return A pointer to the image that was loaded from memory + public: std::shared_ptr TextureData() const; + /// \brief Get a texture image /// \return The name of the texture image (if one exists) or an empty /// string diff --git a/graphics/include/gz/common/MeshManager.hh b/graphics/include/gz/common/MeshManager.hh index 5185dd4c7..07e770cce 100644 --- a/graphics/include/gz/common/MeshManager.hh +++ b/graphics/include/gz/common/MeshManager.hh @@ -45,7 +45,11 @@ namespace gz class SubMesh; /// \class MeshManager MeshManager.hh gz/common/MeshManager.hh - /// \brief Maintains and manages all meshes + /// \brief Maintains and manages all meshes. Supported mesh formats are + /// STL (STLA, STLB), COLLADA, OBJ, GLTF (GLB) and FBX. By default only GLTF + /// and FBX are loaded using assimp loader, however if GZ_MESH_FORCE_ASSIMP + /// environment variable is set, then MeshManager will use assimp loader for + /// all supported mesh formats. class GZ_COMMON_GRAPHICS_VISIBLE MeshManager : public SingletonT { @@ -240,8 +244,12 @@ namespace gz const gz::math::Vector2d &_segments, const gz::math::Vector2d &_uvTile); + /// \brief Sets the forceAssimp flag by reading the GZ_MESH_FORCE_ASSIMP + /// environment variable. If forceAssimp true, MeshManager uses Assimp + /// for loading all mesh formats, otherwise only for GLTF and FBX. + public: void SetAssimpEnvs(); + /// \brief Tesselate a 2D mesh - /// /// Makes a zigzag pattern compatible with strips /// \param[in] _sm the mesh to tesselate /// \param[in] _meshWith mesh width diff --git a/graphics/include/gz/common/Pbr.hh b/graphics/include/gz/common/Pbr.hh index 95bcb473f..ac3e42091 100644 --- a/graphics/include/gz/common/Pbr.hh +++ b/graphics/include/gz/common/Pbr.hh @@ -17,11 +17,13 @@ #ifndef GZ_COMMON_PBR_HH_ #define GZ_COMMON_PBR_HH_ +#include #include #include #include +#include namespace gz { @@ -84,12 +86,20 @@ namespace common /// has not been specified. public: std::string NormalMap() const; + /// \brief Gets the normal map data, + /// if the texture was loaded from memory, otherwise a nullptr + /// \return A pointer to the image that was loaded from memory + public: std::shared_ptr NormalMapData() const; + /// \brief Set the normal map filename. /// \param[in] _map Filename of the normal map. /// \param[in] _space Space that the normal map is defined in. /// Defaults to tangent space. + /// \param[in] _img The image containing the texture if image has been + /// loaded in memory public: void SetNormalMap(const std::string &_map, - NormalMapSpace _space = NormalMapSpace::TANGENT); + NormalMapSpace _space = NormalMapSpace::TANGENT, + const std::shared_ptr &_img = nullptr); /// \brief Get the normal map type, either tangent or object space /// \return Space that the normal map is defined in @@ -123,7 +133,15 @@ namespace common /// \brief Set the roughness map filename for metal workflow. /// \param[in] _map Filename of the roughness map. - public: void SetRoughnessMap(const std::string &_map); + /// \param[in] _img The image containing the texture if image has been + /// loaded in memory + public: void SetRoughnessMap(const std::string &_map, + const std::shared_ptr &_img = nullptr); + + /// \brief Gets the roughness map data, + /// if the texture was loaded from memory, otherwise a nullptr + /// \return A pointer to the image that was loaded from memory + public: std::shared_ptr RoughnessMapData() const; /// \brief Get the metalness map filename for metal workflow. This will be /// an empty string if a metalness map has not been set. @@ -133,7 +151,15 @@ namespace common /// \brief Set the metalness map filename for metal workflow. /// \param[in] _map Filename of the metalness map. - public: void SetMetalnessMap(const std::string &_map); + /// \param[in] _img The image containing the texture if image has been + /// loaded in memory + public: void SetMetalnessMap(const std::string &_map, + const std::shared_ptr &_img = nullptr); + + /// \brief Gets the metalness map data, + /// if the texture was loaded from memory, otherwise a nullptr + /// \return A pointer to the image that was loaded from memory + public: std::shared_ptr MetalnessMapData() const; /// \brief Get the emissive map filename. This will be an empty string /// if an emissive map has not been set. @@ -143,7 +169,15 @@ namespace common /// \brief Set the emissive map filename. /// \param[in] _map Filename of the emissive map. - public: void SetEmissiveMap(const std::string &_map); + /// \param[in] _img The image containing the texture if image has been + /// loaded in memory + public: void SetEmissiveMap(const std::string &_map, + const std::shared_ptr &_img = nullptr); + + /// \brief Gets the emissive map data, + /// if the texture was loaded from memory, otherwise a nullptr + /// \return A pointer to the image that was loaded from memory + public: std::shared_ptr EmissiveMapData() const; /// \brief Get the light map filename. This will be an empty string /// if an light map has not been set. @@ -151,10 +185,18 @@ namespace common /// map has not been specified. public: std::string LightMap() const; + /// \brief Gets the light map data, + /// if the texture was loaded from memory, otherwise a nullptr + /// \return A pointer to the image that was loaded from memory + public: std::shared_ptr LightMapData() const; + /// \brief Set the light map filename. /// \param[in] _map Filename of the light map. /// \param[in] _uvSet Index of the texture coordinate set - public: void SetLightMap(const std::string &_map, unsigned int _uvSet = 0u); + /// \param[in] _img The image containing the texture if image has been + /// loaded in memory + public: void SetLightMap(const std::string &_map, unsigned int _uvSet = 0u, + const std::shared_ptr &_img = nullptr); /// \brief Get the light map texture coordinate set. /// \return Index of the light map texture coordinate set diff --git a/graphics/src/AssimpLoader.cc b/graphics/src/AssimpLoader.cc new file mode 100644 index 000000000..caa2662a8 --- /dev/null +++ b/graphics/src/AssimpLoader.cc @@ -0,0 +1,733 @@ +/* + * 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 "gz/common/graphics/Types.hh" +#include "gz/common/AssimpLoader.hh" +#include "gz/common/Console.hh" +#include "gz/common/Image.hh" +#include "gz/common/Material.hh" +#include "gz/common/Mesh.hh" +#include "gz/common/Skeleton.hh" +#include "gz/common/SkeletonAnimation.hh" +#include "gz/common/SubMesh.hh" +#include "gz/common/SystemPaths.hh" +#include "gz/common/Util.hh" + +#ifndef GZ_ASSIMP_PRE_5_2_0 + #include // GLTF specific material properties +#endif +#include // C++ importer interface +#include // Post processing flags +#include // Output data structure + +// Disable warning for converting double to unsigned char +#ifdef _WIN32 + #pragma warning( disable : 4244 ) +#endif + +namespace gz +{ +namespace common +{ + +using ImagePtr = std::shared_ptr; + +/// \brief Private data for the AssimpLoader class +class AssimpLoader::Implementation +{ + /// \brief the Assimp importer used to parse meshes + public: Assimp::Importer importer; + + /// \brief Convert a color from assimp implementation to Ignition common + /// \param[in] _color the assimp color to convert + /// \return the matching math::Color + public: math::Color ConvertColor(aiColor4D& _color) const; + + /// \brief Convert a matrix from assimp implementation to gz::Math + /// \param[in] _matrix the assimp matrix to convert + /// \return the converted math::Matrix4d + public: math::Matrix4d ConvertTransform(const aiMatrix4x4& _matrix) const; + + /// \brief Convert from assimp to gz::common::Material + /// \param[in] _scene the assimp scene + /// \param[in] _matIdx index of the material in the scene + /// \param[in] _path path where the mesh is located + /// \return pointer to the converted common::Material + public: MaterialPtr CreateMaterial(const aiScene* _scene, + unsigned _matIdx, + const std::string& _path) const; + + /// \brief Load a texture embedded in a mesh (i.e. for GLB format) + /// into a gz::common::Image + /// \param[in] _texture the assimp texture object + /// \return Pointer to a common::Image containing the texture + public: ImagePtr LoadEmbeddedTexture(const aiTexture* _texture) const; + + /// \brief Utility function to generate a texture name for both embedded + /// and external textures + /// \param[in] _scene the assimp scene + /// \param[in] _mat the assimp material + /// \param[in] _type the type of texture (i.e. Diffuse, Metal) + /// \return the generated texture name + public: std::string GenerateTextureName(const aiScene* _scene, + aiMaterial* _mat, + const std::string& _type) const; + + /// \brief Function to parse texture information and load it if embedded + /// \param[in] _scene the assimp scene + /// \param[in] _texturePath the path where the texture is located + /// \param[in] _textureName the name of the texture + /// \return a pair containing the name of the texture and a pointer to the + /// image data, if the texture was loaded in memory + public: std::pair + LoadTexture(const aiScene* _scene, + const aiString& _texturePath, + const std::string& _textureName) const; + + /// \brief Function to split a gltf metallicroughness map into + /// a metalness and roughness map + /// \param[in] _img the image to split + /// \return a pair of image pointers with the first being the metalness + /// map and the second being the roughness map + public: std::pair + SplitMetallicRoughnessMap(const common::Image& _img) const; + + /// \brief Convert an assimp mesh into a gz::common::SubMesh + /// \param[in] _assimpMesh the assimp mesh to load + /// \param[in] _transform the node transform for the mesh + /// \return the converted common::Submesh + public: SubMesh CreateSubMesh(const aiMesh* _assimpMesh, + const math::Matrix4d& _transform) const; + + /// \brief Recursively create submeshes scene starting from the root node + /// \param[in] _scene the assimp scene + /// \param[in] _node the node being processed + /// \param[in] _transform the transform of the node being processed + /// \param[out] _mesh the common::Mesh to edit + public: void RecursiveCreate(const aiScene* _scene, + const aiNode* _node, + const math::Matrix4d& _transform, + Mesh* _mesh) const; + + /// \brief Recursively create the skeleton starting from the root node + /// \param[in] _node the node being processed + /// \param[in] _parent the parent skeleton node + /// \param[in] _transform the transform of the current node + /// \param[in] _boneNames set of bone names, used to skip nodes without a bone + public: void RecursiveSkeletonCreate( + const aiNode* _node, + SkeletonNode* _parent, + const math::Matrix4d& _transform, + const std::unordered_set &_boneNames) const; + + /// \brief Recursively store the bone names starting from the root node + /// to make sure that only nodes that map to a bone are added to the skeleton + /// \param[in] _scene the assimp scene + /// \param[in] _node the node being processed + /// \param[out] _boneNames set of bone names populated while recursing + public: void RecursiveStoreBoneNames( + const aiScene *_scene, + const aiNode* _node, + std::unordered_set& _boneNames) const; + + /// \brief Apply the the inv bind transform to the skeleton pose. + /// \remarks have to set the model transforms starting from the root in + /// breadth first order. Because setting the model transform also updates + /// the transform based on the parent's inv model transform. Setting the + /// child before the parent results in the child's transform being + /// calculated from the "old" parent model transform. + /// \param[in] _skeleton the skeleton to work on + public: void ApplyInvBindTransform(SkeletonPtr _skeleton) const; +}; + +////////////////////////////////////////////////// +// Utility function to convert to std::string from aiString +static std::string ToString(const aiString& str) +{ + return std::string(str.C_Str()); +} + +////////////////////////////////////////////////// +math::Color AssimpLoader::Implementation::ConvertColor(aiColor4D& _color) const +{ + math::Color col(_color.r, _color.g, _color.b, _color.a); + return col; +} + +////////////////////////////////////////////////// +math::Matrix4d AssimpLoader::Implementation::ConvertTransform( + const aiMatrix4x4& _sm) const +{ + return math::Matrix4d( + _sm.a1, _sm.a2, _sm.a3, _sm.a4, + _sm.b1, _sm.b2, _sm.b3, _sm.b4, + _sm.c1, _sm.c2, _sm.c3, _sm.c4, + _sm.d1, _sm.d2, _sm.d3, _sm.d4); +} + +////////////////////////////////////////////////// +void AssimpLoader::Implementation::RecursiveCreate(const aiScene* _scene, + const aiNode* _node, const math::Matrix4d& _transform, Mesh* _mesh) const +{ + if (!_node) + return; + // Visit this node, add the submesh + for (unsigned meshIdx = 0; meshIdx < _node->mNumMeshes; ++meshIdx) + { + auto assimpMeshIdx = _node->mMeshes[meshIdx]; + auto& assimpMesh = _scene->mMeshes[assimpMeshIdx]; + auto nodeName = ToString(_node->mName); + auto subMesh = this->CreateSubMesh(assimpMesh, _transform); + subMesh.SetName(nodeName); + // Now add the bones to the skeleton + if (assimpMesh->HasBones() && _scene->HasAnimations()) + { + // TODO(luca) merging skeletons here + auto skeleton = _mesh->MeshSkeleton(); + // TODO(luca) Append to existing skeleton if multiple submeshes? + skeleton->SetNumVertAttached(subMesh.VertexCount()); + // Now add the bone weights + for (unsigned boneIdx = 0; boneIdx < assimpMesh->mNumBones; ++boneIdx) + { + auto& bone = assimpMesh->mBones[boneIdx]; + auto boneNodeName = ToString(bone->mName); + // Apply inverse bind transform to the matching node + SkeletonNode *skelNode = + skeleton->NodeByName(boneNodeName); + if (skelNode == nullptr) + continue; + skelNode->SetInverseBindTransform( + this->ConvertTransform(bone->mOffsetMatrix)); + for (unsigned weightIdx = 0; weightIdx < bone->mNumWeights; ++weightIdx) + { + auto vertexWeight = bone->mWeights[weightIdx]; + skeleton->AddVertNodeWeight( + vertexWeight.mVertexId, boneNodeName, vertexWeight.mWeight); + } + } + // Add node assignment to mesh + for (unsigned vertexIdx = 0; vertexIdx < subMesh.VertexCount(); + ++vertexIdx) + { + for (unsigned i = 0; i < skeleton->VertNodeWeightCount(vertexIdx); ++i) + { + std::pair nodeWeight = + skeleton->VertNodeWeight(vertexIdx, i); + SkeletonNode *node = + skeleton->NodeByName(nodeWeight.first); + subMesh.AddNodeAssignment(vertexIdx, + node->Handle(), nodeWeight.second); + } + } + } + _mesh->AddSubMesh(std::move(subMesh)); + } + + // Iterate over children + for (unsigned childIdx = 0; childIdx < _node->mNumChildren; ++childIdx) + { + // Calculate the transform + auto& child_node = _node->mChildren[childIdx]; + auto nodeTrans = this->ConvertTransform(child_node->mTransformation); + nodeTrans = _transform * nodeTrans; + + // Finally recursive call to explore subnode + this->RecursiveCreate(_scene, child_node, nodeTrans, _mesh); + } +} + +void AssimpLoader::Implementation::RecursiveStoreBoneNames( + const aiScene *_scene, const aiNode *_node, + std::unordered_set& _boneNames) const +{ + if (!_node) + return; + + for (unsigned meshIdx = 0; meshIdx < _node->mNumMeshes; ++meshIdx) + { + auto assimpMeshIdx = _node->mMeshes[meshIdx]; + auto assimpMesh = _scene->mMeshes[assimpMeshIdx]; + for (unsigned boneIdx = 0; boneIdx < assimpMesh->mNumBones; ++boneIdx) + { + auto bone = assimpMesh->mBones[boneIdx]; + _boneNames.insert(ToString(bone->mName)); + } + } + + // Iterate over children + for (unsigned childIdx = 0; childIdx < _node->mNumChildren; ++childIdx) + { + auto child_node = _node->mChildren[childIdx]; + // Finally recursive call to explore subnode + this->RecursiveStoreBoneNames(_scene, child_node, _boneNames); + } +} + +////////////////////////////////////////////////// +void AssimpLoader::Implementation::RecursiveSkeletonCreate(const aiNode* _node, + SkeletonNode* _parent, const math::Matrix4d& _transform, + const std::unordered_set &_boneNames) const +{ + if (_node == nullptr || _parent == nullptr) + return; + // First explore this node + auto nodeName = ToString(_node->mName); + auto boneExist = _boneNames.find(nodeName) != _boneNames.end(); + auto nodeTrans = this->ConvertTransform(_node->mTransformation); + auto skelNode = _parent; + + if (boneExist) + { + skelNode = new SkeletonNode( + _parent, nodeName, nodeName, SkeletonNode::JOINT); + skelNode->SetTransform(nodeTrans); + } + + nodeTrans = _transform * nodeTrans; + + for (unsigned childIdx = 0; childIdx < _node->mNumChildren; ++childIdx) + { + this->RecursiveSkeletonCreate( + _node->mChildren[childIdx], skelNode, nodeTrans, _boneNames); + } +} + +////////////////////////////////////////////////// +MaterialPtr AssimpLoader::Implementation::CreateMaterial( + const aiScene* _scene, unsigned _matIdx, const std::string& _path) const +{ + MaterialPtr mat = std::make_shared(); + aiColor4D color; + auto& assimpMat = _scene->mMaterials[_matIdx]; + auto ret = assimpMat->Get(AI_MATKEY_COLOR_DIFFUSE, color); + if (ret == AI_SUCCESS) + { + mat->SetDiffuse(this->ConvertColor(color)); + } + ret = assimpMat->Get(AI_MATKEY_COLOR_AMBIENT, color); + if (ret == AI_SUCCESS) + { + mat->SetAmbient(this->ConvertColor(color)); + } + ret = assimpMat->Get(AI_MATKEY_COLOR_SPECULAR, color); + if (ret == AI_SUCCESS) + { + mat->SetSpecular(this->ConvertColor(color)); + } + ret = assimpMat->Get(AI_MATKEY_COLOR_EMISSIVE, color); + if (ret == AI_SUCCESS) + { + mat->SetEmissive(this->ConvertColor(color)); + } + float shininess; + ret = assimpMat->Get(AI_MATKEY_SHININESS, shininess); + if (ret == AI_SUCCESS) + { + mat->SetShininess(shininess); + } + float opacity = 1.0; + ret = assimpMat->Get(AI_MATKEY_OPACITY, opacity); + mat->SetTransparency(1.0 - opacity); + mat->SetBlendFactors(opacity, 1.0 - opacity); + // TODO(luca) more than one texture, Gazebo assumes UV index 0 + Pbr pbr; + aiString texturePath(_path.c_str()); + ret = assimpMat->GetTexture(aiTextureType_DIFFUSE, 0, &texturePath); + // TODO(luca) check other arguments, + // type of mappings to be UV, uv index, blend mode + if (ret == AI_SUCCESS) + { + // Check if the texture is embedded or not + auto [texName, texData] = this->LoadTexture(_scene, + texturePath, this->GenerateTextureName(_scene, assimpMat, "Diffuse")); + if (texData != nullptr) + mat->SetTextureImage(texName, texData); + else + mat->SetTextureImage(texName, _path); +#ifndef GZ_ASSIMP_PRE_5_2_0 + // Now set the alpha from texture, if enabled, only supported in GLTF + aiString alphaMode; + auto paramRet = assimpMat->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode); + if (paramRet == AI_SUCCESS) + { + // Only enable if it's set to MASK, BLEND not supported yet + if (strcmp(alphaMode.C_Str(), "MASK") == 0) + { + double alphaCutoff = mat->AlphaThreshold(); + bool twoSided = mat->TwoSidedEnabled(); + // Ignore return value, parameter unchanged if value is not set + assimpMat->Get(AI_MATKEY_GLTF_ALPHACUTOFF, alphaCutoff); + assimpMat->Get(AI_MATKEY_TWOSIDED, twoSided); + mat->SetAlphaFromTexture(true, alphaCutoff, twoSided); + } + } +#endif + } +#ifndef GZ_ASSIMP_PRE_5_2_0 + // Edge case for GLTF, Metal and Rough texture are embedded in a + // MetallicRoughness texture with metalness in B and roughness in G + // Open, preprocess and split into metal and roughness map + ret = assimpMat->GetTexture( + AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, + &texturePath); + if (ret == AI_SUCCESS) + { + auto [texName, texData] = this->LoadTexture(_scene, texturePath, + this->GenerateTextureName(_scene, assimpMat, "MetallicRoughness")); + // Load it into a common::Image then split it + auto texImg = + texData != nullptr ? texData : std::make_shared(texName); + auto [metalTexture, roughTexture] = + this->SplitMetallicRoughnessMap(*texImg); + pbr.SetMetalnessMap( + this->GenerateTextureName(_scene, assimpMat, "Metalness"), + metalTexture); + pbr.SetRoughnessMap( + this->GenerateTextureName(_scene, assimpMat, "Roughness"), + roughTexture); + } + else + { + // Load the textures separately + ret = assimpMat->GetTexture(aiTextureType_METALNESS, 0, &texturePath); + if (ret == AI_SUCCESS) + { + auto [texName, texData] = this->LoadTexture(_scene, texturePath, + this->GenerateTextureName(_scene, assimpMat, "Metalness")); + pbr.SetMetalnessMap(texName, texData); + } + ret = assimpMat->GetTexture( + aiTextureType_DIFFUSE_ROUGHNESS, 0, &texturePath); + if (ret == AI_SUCCESS) + { + auto [texName, texData] = this->LoadTexture(_scene, texturePath, + this->GenerateTextureName(_scene, assimpMat, "Roughness")); + pbr.SetRoughnessMap(texName, texData); + } + } +#endif + ret = assimpMat->GetTexture(aiTextureType_NORMALS, 0, &texturePath); + if (ret == AI_SUCCESS) + { + auto [texName, texData] = this->LoadTexture(_scene, texturePath, + this->GenerateTextureName(_scene, assimpMat, "Normal")); + // TODO(luca) different normal map spaces + pbr.SetNormalMap(texName, NormalMapSpace::TANGENT, texData); + } + ret = assimpMat->GetTexture(aiTextureType_EMISSIVE, 0, &texturePath); + if (ret == AI_SUCCESS) + { + auto [texName, texData] = this->LoadTexture(_scene, texturePath, + this->GenerateTextureName(_scene, assimpMat, "Emissive")); + pbr.SetEmissiveMap(texName, texData); + } + unsigned int uvIdx = 0; + ret = assimpMat->GetTexture( + aiTextureType_LIGHTMAP, 0, &texturePath, NULL, &uvIdx); + if (ret == AI_SUCCESS) + { + auto [texName, texData] = this->LoadTexture(_scene, texturePath, + this->GenerateTextureName(_scene, assimpMat, "Lightmap")); + pbr.SetLightMap(texName, uvIdx, texData); + } +#ifndef GZ_ASSIMP_PRE_5_2_0 + double value; + ret = assimpMat->Get(AI_MATKEY_METALLIC_FACTOR, value); + if (ret == AI_SUCCESS) + { + pbr.SetMetalness(value); + } + ret = assimpMat->Get(AI_MATKEY_ROUGHNESS_FACTOR, value); + if (ret == AI_SUCCESS) + { + pbr.SetRoughness(value); + } +#endif + mat->SetPbrMaterial(pbr); + return mat; +} + +////////////////////////////////////////////////// +std::pair AssimpLoader::Implementation::LoadTexture( + const aiScene* _scene, + const aiString& _texturePath, + const std::string& _textureName) const +{ + std::pair ret; + // Check if the texture is embedded or not + auto embeddedTexture = _scene->GetEmbeddedTexture(_texturePath.C_Str()); + if (embeddedTexture) + { + // Load embedded texture + ret.first = _textureName; + ret.second = this->LoadEmbeddedTexture(embeddedTexture); + } + else + { + ret.first = ToString(_texturePath); + } + return ret; +} + +std::pair + AssimpLoader::Implementation::SplitMetallicRoughnessMap( + const common::Image& _img) const +{ + std::pair ret; + // Metalness in B roughness in G + const auto width = _img.Width(); + const auto height = _img.Height(); + const auto bytesPerPixel = 4; + + std::vector metalnessData(width * height * bytesPerPixel); + std::vector roughnessData(width * height * bytesPerPixel); + + for (unsigned int x = 0; x < width; ++x) + { + for (unsigned int y = 0; y < height; ++y) + { + // RGBA so 4 bytes per pixel, alpha fully opaque + auto baseIndex = bytesPerPixel * (x * height + y); + auto color = _img.Pixel(x, y); + metalnessData[baseIndex] = color.B() * 255.0; + metalnessData[baseIndex + 1] = color.B() * 255.0; + metalnessData[baseIndex + 2] = color.B() * 255.0; + metalnessData[baseIndex + 3] = 255; + roughnessData[baseIndex] = color.G() * 255.0; + roughnessData[baseIndex + 1] = color.G() * 255.0; + roughnessData[baseIndex + 2] = color.G() * 255.0; + roughnessData[baseIndex + 3] = 255; + } + } + // First is metal, second is rough + ret.first = std::make_shared(); + ret.first->SetFromData(&metalnessData[0], width, height, Image::RGBA_INT8); + ret.second = std::make_shared(); + ret.second->SetFromData(&roughnessData[0], width, height, Image::RGBA_INT8); + return ret; +} + +////////////////////////////////////////////////// +ImagePtr AssimpLoader::Implementation::LoadEmbeddedTexture( + const aiTexture* _texture) const +{ + auto img = std::make_shared(); + if (_texture->mHeight == 0) + { + if (_texture->CheckFormat("png")) + { + img->SetFromCompressedData((unsigned char*)_texture->pcData, + _texture->mWidth, Image::PixelFormatType::COMPRESSED_PNG); + } + } + return img; +} + +////////////////////////////////////////////////// +std::string AssimpLoader::Implementation::GenerateTextureName( + const aiScene* _scene, aiMaterial* _mat, const std::string& _type) const +{ + return ToString(_scene->mRootNode->mName) + "_" + ToString(_mat->GetName()) + + "_" + _type; +} + +SubMesh AssimpLoader::Implementation::CreateSubMesh( + const aiMesh* _assimpMesh, const math::Matrix4d& _transform) const +{ + SubMesh subMesh; + math::Matrix4d rot = _transform; + rot.SetTranslation(math::Vector3d::Zero); + // Now create the submesh + for (unsigned vertexIdx = 0; vertexIdx < _assimpMesh->mNumVertices; + ++vertexIdx) + { + // Add the vertex + math::Vector3d vertex; + math::Vector3d normal; + vertex.X(_assimpMesh->mVertices[vertexIdx].x); + vertex.Y(_assimpMesh->mVertices[vertexIdx].y); + vertex.Z(_assimpMesh->mVertices[vertexIdx].z); + normal.X(_assimpMesh->mNormals[vertexIdx].x); + normal.Y(_assimpMesh->mNormals[vertexIdx].y); + normal.Z(_assimpMesh->mNormals[vertexIdx].z); + vertex = _transform * vertex; + normal = rot * normal; + normal.Normalize(); + subMesh.AddVertex(vertex); + subMesh.AddNormal(normal); + // Iterate over sets of texture coordinates + int uvIdx = 0; + while(_assimpMesh->HasTextureCoords(uvIdx)) + { + math::Vector3d texcoords; + texcoords.X(_assimpMesh->mTextureCoords[uvIdx][vertexIdx].x); + texcoords.Y(_assimpMesh->mTextureCoords[uvIdx][vertexIdx].y); + // TODO(luca) why do we need 1.0 - Y? + subMesh.AddTexCoordBySet(texcoords.X(), 1.0 - texcoords.Y(), uvIdx); + ++uvIdx; + } + } + for (unsigned faceIdx = 0; faceIdx < _assimpMesh->mNumFaces; ++faceIdx) + { + auto& face = _assimpMesh->mFaces[faceIdx]; + subMesh.AddIndex(face.mIndices[0]); + subMesh.AddIndex(face.mIndices[1]); + subMesh.AddIndex(face.mIndices[2]); + } + subMesh.SetMaterialIndex(_assimpMesh->mMaterialIndex); + return subMesh; +} + +////////////////////////////////////////////////// +AssimpLoader::AssimpLoader() +: MeshLoader(), dataPtr(utils::MakeUniqueImpl()) +{ + this->dataPtr->importer.SetPropertyBool(AI_CONFIG_PP_FD_REMOVE, true); + this->dataPtr->importer.SetPropertyBool( + AI_CONFIG_IMPORT_REMOVE_EMPTY_BONES, false); +} + +////////////////////////////////////////////////// +AssimpLoader::~AssimpLoader() +{ +} + +////////////////////////////////////////////////// +Mesh *AssimpLoader::Load(const std::string &_filename) +{ + Mesh *mesh = new Mesh(); + std::string path = common::parentPath(_filename); + const aiScene* scene = this->dataPtr->importer.ReadFile(_filename, + aiProcess_JoinIdenticalVertices | + aiProcess_RemoveRedundantMaterials | + aiProcess_SortByPType | +#ifndef GZ_ASSIMP_PRE_5_2_0 + aiProcess_PopulateArmatureData | +#endif + aiProcess_Triangulate | + aiProcess_GenNormals | + 0); + if (scene == nullptr) + { + ignerr << "Unable to import mesh [" << _filename << "]" << std::endl; + return mesh; + } + auto& rootNode = scene->mRootNode; + auto rootName = ToString(rootNode->mName); + auto transform = scene->mRootNode->mTransformation; + aiVector3D rootScaling, rootAxis, rootPos; + float angle; + transform.Decompose(rootScaling, rootAxis, angle, rootPos); + // drop rotation, but keep scaling and position + // TODO(luca) it seems imported assets are rotated by 90 degrees + // as documented here https://github.com/assimp/assimp/issues/849 + // remove workaround when fixed + transform = aiMatrix4x4(rootScaling, aiQuaternion(), rootPos); + + auto rootTransform = this->dataPtr->ConvertTransform(transform); + + // Add the materials first + for (unsigned _matIdx = 0; _matIdx < scene->mNumMaterials; ++_matIdx) + { + auto mat = this->dataPtr->CreateMaterial(scene, _matIdx, path); + mesh->AddMaterial(mat); + } + // Create the skeleton + { + std::unordered_set boneNames; + this->dataPtr->RecursiveStoreBoneNames(scene, rootNode, boneNames); + auto rootSkelNode = new SkeletonNode( + nullptr, rootName, rootName, SkeletonNode::NODE); + rootSkelNode->SetTransform(rootTransform); + rootSkelNode->SetModelTransform(rootTransform); + for (unsigned childIdx = 0; childIdx < rootNode->mNumChildren; ++childIdx) + { + // First populate the skeleton with the node transforms + this->dataPtr->RecursiveSkeletonCreate( + rootNode->mChildren[childIdx], rootSkelNode, + rootTransform, boneNames); + } + rootSkelNode->SetParent(nullptr); + + SkeletonPtr rootSkeleton = std::make_shared(rootSkelNode); + mesh->SetSkeleton(rootSkeleton); + } + // Now create the meshes + // Recursive call to keep track of transforms, + // mesh is passed by reference and edited throughout + this->dataPtr->RecursiveCreate(scene, rootNode, rootTransform, mesh); + // Add the animations + for (unsigned animIdx = 0; animIdx < scene->mNumAnimations; ++animIdx) + { + auto& anim = scene->mAnimations[animIdx]; + auto animName = ToString(anim->mName); + SkeletonAnimation* skelAnim = new SkeletonAnimation(animName); + for (unsigned chanIdx = 0; chanIdx < anim->mNumChannels; ++chanIdx) + { + auto& animChan = anim->mChannels[chanIdx]; + auto chanName = ToString(animChan->mNodeName); + for (unsigned keyIdx = 0; keyIdx < animChan->mNumPositionKeys; ++keyIdx) + { + // Note, Scaling keys are not supported right now + // Compute the position into a math pose + auto& posKey = animChan->mPositionKeys[keyIdx]; + auto& quatKey = animChan->mRotationKeys[keyIdx]; + math::Vector3d pos(posKey.mValue.x, posKey.mValue.y, posKey.mValue.z); + math::Quaterniond quat(quatKey.mValue.w, quatKey.mValue.x, + quatKey.mValue.y, quatKey.mValue.z); + math::Pose3d pose(pos, quat); + // Time is in ms + skelAnim->AddKeyFrame(chanName, posKey.mTime / 1000.0, pose); + } + } + mesh->MeshSkeleton()->AddAnimation(skelAnim); + } + + this->dataPtr->ApplyInvBindTransform(mesh->MeshSkeleton()); + + return mesh; +} + +///////////////////////////////////////////////// +void AssimpLoader::Implementation::ApplyInvBindTransform( + SkeletonPtr _skeleton) const + +{ + std::queue queue; + queue.push(_skeleton->RootNode()); + + while (!queue.empty()) + { + SkeletonNode *node = queue.front(); + queue.pop(); + if (nullptr == node) + continue; + + if (node->HasInvBindTransform()) + { + node->SetModelTransform(node->InverseBindTransform().Inverse(), false); + } + for (unsigned int i = 0; i < node->ChildCount(); i++) + queue.push(node->Child(i)); + } +} + +} +} diff --git a/graphics/src/AssimpLoader_TEST.cc b/graphics/src/AssimpLoader_TEST.cc new file mode 100644 index 000000000..a32a0a331 --- /dev/null +++ b/graphics/src/AssimpLoader_TEST.cc @@ -0,0 +1,699 @@ +/* + * 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 "gz/common/Material.hh" +#include "gz/common/Mesh.hh" +#include "gz/common/Skeleton.hh" +#include "gz/common/SkeletonAnimation.hh" +#include "gz/common/SubMesh.hh" +#include "gz/common/AssimpLoader.hh" + +#include "gz/common/testing/AutoLogFixture.hh" +#include "gz/common/testing/TestPaths.hh" + +using namespace gz; +class AssimpLoader : public common::testing::AutoLogFixture { }; + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBox) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box.dae")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + EXPECT_EQ(math::Vector3d(1, 1, 1), mesh->Max()); + EXPECT_EQ(math::Vector3d(-1, -1, -1), mesh->Min()); + // 36 vertices, 24 unique, 12 shared. + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(24u, mesh->NormalCount()); + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(0u, mesh->TexCoordCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + // Make sure we can read a submesh name + EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, Material) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box.dae")); + ASSERT_TRUE(mesh); + + EXPECT_EQ(mesh->MaterialCount(), 1u); + + common::MaterialPtr mat = mesh->MaterialByIndex(0u); + ASSERT_TRUE(mat != nullptr); + + // Make sure we read the specular value + EXPECT_EQ(math::Color(0.0, 0.0, 0.0, 1.0), mat->Ambient()); + EXPECT_EQ(math::Color(0.64f, 0.64f, 0.64f, 1.0f), mat->Diffuse()); + EXPECT_EQ(math::Color(0.5, 0.5, 0.5, 1.0), mat->Specular()); + EXPECT_EQ(math::Color(0.0, 0.0, 0.0, 1.0), mat->Emissive()); + EXPECT_DOUBLE_EQ(50.0, mat->Shininess()); + // transparent: opaque="A_ONE", color=[1 1 1 1] + // transparency: 1.0 + // resulting transparency value = (1 - color.a * transparency) + EXPECT_DOUBLE_EQ(0.0, mat->Transparency()); + double srcFactor = -1; + double dstFactor = -1; + mat->BlendFactors(srcFactor, dstFactor); + EXPECT_DOUBLE_EQ(1.0, srcFactor); + EXPECT_DOUBLE_EQ(0.0, dstFactor); + + common::Mesh *meshOpaque = loader.Load( + common::testing::TestFile("data", "box_opaque.dae")); + ASSERT_TRUE(meshOpaque); + + EXPECT_EQ(meshOpaque->MaterialCount(), 1u); + + common::MaterialPtr matOpaque = meshOpaque->MaterialByIndex(0u); + ASSERT_TRUE(matOpaque != nullptr); + + // Make sure we read the specular value + EXPECT_EQ(math::Color(0.0, 0.0, 0.0, 1.0), matOpaque->Ambient()); + EXPECT_EQ(math::Color(0.64f, 0.64f, 0.64f, 1.0f), matOpaque->Diffuse()); + EXPECT_EQ(math::Color(0.5, 0.5, 0.5, 1.0), matOpaque->Specular()); + EXPECT_EQ(math::Color(0.0, 0.0, 0.0, 1.0), matOpaque->Emissive()); + EXPECT_DOUBLE_EQ(50.0, matOpaque->Shininess()); + // transparent: opaque="A_ONE", color=[1 1 1 1] + // transparency: not specified, defaults to 1.0 + // resulting transparency value = (1 - color.a * transparency) + EXPECT_DOUBLE_EQ(0.0, matOpaque->Transparency()); + srcFactor = -1; + dstFactor = -1; + matOpaque->BlendFactors(srcFactor, dstFactor); + EXPECT_DOUBLE_EQ(1.0, srcFactor); + EXPECT_DOUBLE_EQ(0.0, dstFactor); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, ShareVertices) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box.dae")); + + // check number of shared vertices + std::set uniqueIndices; + int shared = 0; + for (unsigned int i = 0; i < mesh->SubMeshCount(); ++i) + { + const std::shared_ptr subMesh = + mesh->SubMeshByIndex(i).lock(); + for (unsigned int j = 0; j < subMesh->IndexCount(); ++j) + { + if (uniqueIndices.find(subMesh->Index(j)) == uniqueIndices.end()) + { + uniqueIndices.insert(subMesh->Index(j)); + } + else + { + shared++; + } + } + } + EXPECT_EQ(shared, 12); + EXPECT_EQ(uniqueIndices.size(), 24u); + + // check all vertices are unique + for (unsigned int i = 0; i < mesh->SubMeshCount(); ++i) + { + const std::shared_ptr subMesh = + mesh->SubMeshByIndex(i).lock(); + for (unsigned int j = 0; j < subMesh->VertexCount(); ++j) + { + math::Vector3d v = subMesh->Vertex(j); + math::Vector3d n = subMesh->Normal(j); + + // Verify there is no other vertex with the same position AND normal + for (unsigned int k = j+1; k < subMesh->VertexCount(); ++k) + { + if (v == subMesh->Vertex(k)) + { + EXPECT_TRUE(n != subMesh->Normal(k)); + } + } + } + } +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadZeroCount) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "zero_count.dae")); + ASSERT_TRUE(mesh); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, TexCoordSets) +{ + common::AssimpLoader loader; + // This triangle mesh has multiple uv sets and vertices separated by + // line breaks + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", + "multiple_texture_coordinates_triangle.dae")); + ASSERT_TRUE(mesh); + + EXPECT_EQ(6u, mesh->VertexCount()); + EXPECT_EQ(6u, mesh->NormalCount()); + EXPECT_EQ(6u, mesh->IndexCount()); + EXPECT_EQ(6u, mesh->TexCoordCount()); + EXPECT_EQ(2u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + auto sm = mesh->SubMeshByIndex(0u); + auto subMesh = sm.lock(); + EXPECT_NE(nullptr, subMesh); + EXPECT_EQ(math::Vector3d(0, 0, 0), subMesh->Vertex(0u)); + EXPECT_EQ(math::Vector3d(10, 0, 0), subMesh->Vertex(1u)); + EXPECT_EQ(math::Vector3d(10, 10, 0), subMesh->Vertex(2u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMesh->Normal(0u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMesh->Normal(1u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMesh->Normal(2u)); + EXPECT_EQ(math::Vector2d(0, 1), subMesh->TexCoord(0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMesh->TexCoord(1u)); + EXPECT_EQ(math::Vector2d(0, 1), subMesh->TexCoord(2u)); + + auto smb = mesh->SubMeshByIndex(1u); + auto subMeshB = smb.lock(); + EXPECT_NE(nullptr, subMeshB); + EXPECT_EQ(math::Vector3d(10, 0, 0), subMeshB->Vertex(0u)); + EXPECT_EQ(math::Vector3d(20, 0, 0), subMeshB->Vertex(1u)); + EXPECT_EQ(math::Vector3d(20, 10, 0), subMeshB->Vertex(2u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMeshB->Normal(0u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMeshB->Normal(1u)); + EXPECT_EQ(math::Vector3d(0, 0, 1), subMeshB->Normal(2u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoord(0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoord(1u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoord(2u)); + + EXPECT_TRUE(subMeshB->HasTexCoord(0u)); + EXPECT_TRUE(subMeshB->HasTexCoord(1u)); + EXPECT_TRUE(subMeshB->HasTexCoord(2u)); + EXPECT_FALSE(subMeshB->HasTexCoord(3u)); + + // test texture coordinate set API + EXPECT_EQ(3u, subMeshB->TexCoordSetCount()); + EXPECT_EQ(3u, subMeshB->TexCoordCountBySet(0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoordBySet(0u, 0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoordBySet(1u, 0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoordBySet(2u, 0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoordBySet(1u, 0u)); + EXPECT_EQ(math::Vector2d(0, 1), subMeshB->TexCoordBySet(2u, 0u)); + + EXPECT_TRUE(subMeshB->HasTexCoordBySet(0u, 0u)); + EXPECT_TRUE(subMeshB->HasTexCoordBySet(1u, 0u)); + EXPECT_TRUE(subMeshB->HasTexCoordBySet(2u, 0u)); + EXPECT_FALSE(subMeshB->HasTexCoordBySet(3u, 0u)); + + EXPECT_EQ(3u, subMeshB->TexCoordCountBySet(1u)); + EXPECT_EQ(math::Vector2d(0, 0.5), subMeshB->TexCoordBySet(0u, 1u)); + EXPECT_EQ(math::Vector2d(0, 0.4), subMeshB->TexCoordBySet(1u, 1u)); + EXPECT_EQ(math::Vector2d(0, 0.3), subMeshB->TexCoordBySet(2u, 1u)); + + EXPECT_TRUE(subMeshB->HasTexCoordBySet(0u, 1u)); + EXPECT_TRUE(subMeshB->HasTexCoordBySet(1u, 1u)); + EXPECT_TRUE(subMeshB->HasTexCoordBySet(2u, 1u)); + EXPECT_FALSE(subMeshB->HasTexCoordBySet(3u, 1u)); + + EXPECT_EQ(3u, subMeshB->TexCoordCountBySet(2u)); + EXPECT_EQ(math::Vector2d(0, 0.8), subMeshB->TexCoordBySet(0u, 2u)); + EXPECT_EQ(math::Vector2d(0, 0.7), subMeshB->TexCoordBySet(1u, 2u)); + EXPECT_EQ(math::Vector2d(0, 0.6), subMeshB->TexCoordBySet(2u, 2u)); + + EXPECT_TRUE(subMeshB->HasTexCoordBySet(0u, 2u)); + EXPECT_TRUE(subMeshB->HasTexCoordBySet(1u, 2u)); + EXPECT_TRUE(subMeshB->HasTexCoordBySet(2u, 2u)); + EXPECT_FALSE(subMeshB->HasTexCoordBySet(3u, 2u)); + + subMeshB->SetTexCoordBySet(2u, math::Vector2d(0.1, 0.2), 1u); + EXPECT_EQ(math::Vector2d(0.1, 0.2), subMeshB->TexCoordBySet(2u, 1u)); +} + +// Test fails for assimp below 5.2.0 +#ifndef GZ_ASSIMP_PRE_5_2_0 +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxWithAnimationOutsideSkeleton) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", + "box_with_animation_outside_skeleton.dae")); + + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + EXPECT_LT(0u, mesh->TexCoordCount()); + common::SkeletonPtr skeleton = mesh->MeshSkeleton(); + ASSERT_EQ(1u, skeleton->AnimationCount()); + common::SkeletonAnimation *anim = skeleton->Animation(0); + EXPECT_EQ(1u, anim->NodeCount()); + EXPECT_TRUE(anim->HasNode("Armature")); + auto nodeAnimation = anim->NodeAnimationByName("Armature"); + EXPECT_NE(nullptr, nodeAnimation); + EXPECT_EQ("Armature", nodeAnimation->Name()); + auto poseStart = anim->PoseAt(0.04166662); + math::Matrix4d expectedTrans = math::Matrix4d( + 1, 0, 0, 1, + 0, 1, 0, -1, + 0, 0, 1, 0, + 0, 0, 0, 1); + EXPECT_EQ(expectedTrans, poseStart.at("Armature")); + auto poseEnd = anim->PoseAt(1.666666); + expectedTrans = math::Matrix4d( + 1, 0, 0, 2, + 0, 1, 0, -1, + 0, 0, 1, 0, + 0, 0, 0, 1); + EXPECT_EQ(expectedTrans, poseEnd.at("Armature")); +} +#endif + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxInstControllerWithoutSkeleton) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", + "box_inst_controller_without_skeleton.dae")); + + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + EXPECT_EQ(24u, mesh->TexCoordCount()); + common::SkeletonPtr skeleton = mesh->MeshSkeleton(); + EXPECT_LT(0u, skeleton->NodeCount()); + EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone")); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxMultipleInstControllers) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_multiple_inst_controllers.dae")); + + EXPECT_EQ(72u, mesh->IndexCount()); + EXPECT_EQ(48u, mesh->VertexCount()); + EXPECT_EQ(2u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + EXPECT_EQ(48u, mesh->TexCoordCount()); + + std::shared_ptr submesh = mesh->SubMeshByIndex(0).lock(); + std::shared_ptr submesh2 = mesh->SubMeshByIndex(1).lock(); + EXPECT_EQ(36u, submesh->IndexCount()); + EXPECT_EQ(36u, submesh2->IndexCount()); + EXPECT_EQ(24u, submesh->VertexCount()); + EXPECT_EQ(24u, submesh2->VertexCount()); + EXPECT_EQ(24u, submesh->TexCoordCount()); + EXPECT_EQ(24u, submesh2->TexCoordCount()); + + common::SkeletonPtr skeleton = mesh->MeshSkeleton(); + EXPECT_LT(0u, skeleton->NodeCount()); + EXPECT_NE(nullptr, skeleton->NodeById("Armature_Bone")); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxNestedAnimation) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_nested_animation.dae")); + + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + EXPECT_EQ(24u, mesh->TexCoordCount()); + common::SkeletonPtr skeleton = mesh->MeshSkeleton(); + ASSERT_EQ(1u, mesh->MeshSkeleton()->AnimationCount()); + common::SkeletonAnimation *anim = skeleton->Animation(0); + // Depends on fix in assimp main branch for nested animation naming + // TODO(luca) Fix is merged in assimp main, add when it is re-released + // EXPECT_EQ(anim->Name(), "Armature"); + EXPECT_EQ(1u, anim->NodeCount()); + EXPECT_TRUE(anim->HasNode("Armature_Bone")); + auto nodeAnimation = anim->NodeAnimationByName("Armature_Bone"); + ASSERT_NE(nullptr, nodeAnimation); + EXPECT_EQ("Armature_Bone", nodeAnimation->Name()); + auto poseStart = anim->PoseAt(0); + math::Matrix4d expectedTrans = math::Matrix4d( + 1, 0, 0, 1, + 0, 1, 0, -1, + 0, 0, 1, 0, + 0, 0, 0, 1); + EXPECT_EQ(expectedTrans, poseStart.at("Armature_Bone")); + auto poseEnd = anim->PoseAt(1.666666); + expectedTrans = math::Matrix4d( + 1, 0, 0, 2, + 0, 1, 0, -1, + 0, 0, 1, 0, + 0, 0, 0, 1); + EXPECT_EQ(expectedTrans, poseEnd.at("Armature_Bone")); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxWithDefaultStride) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_with_default_stride.dae")); + ASSERT_NE(mesh, nullptr); + + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + EXPECT_EQ(24u, mesh->TexCoordCount()); + ASSERT_NE(mesh->MeshSkeleton(), nullptr); + // TODO(luca) not working, investigate + // ASSERT_EQ(1u, mesh->MeshSkeleton()->AnimationCount()); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxWithMultipleGeoms) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_with_multiple_geoms.dae")); + + EXPECT_EQ(72u, mesh->IndexCount()); + EXPECT_EQ(48u, mesh->VertexCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + EXPECT_EQ(48u, mesh->TexCoordCount()); + ASSERT_EQ(1u, mesh->MeshSkeleton()->AnimationCount()); + ASSERT_EQ(2u, mesh->SubMeshCount()); + EXPECT_EQ(24u, mesh->SubMeshByIndex(0).lock()->NodeAssignmentsCount()); + EXPECT_EQ(0u, mesh->SubMeshByIndex(1).lock()->NodeAssignmentsCount()); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadBoxWithHierarchicalNodes) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_with_hierarchical_nodes.dae")); + + ASSERT_EQ(5u, mesh->SubMeshCount()); + + // node by itself + EXPECT_EQ("StaticCube", mesh->SubMeshByIndex(0).lock()->Name()); + + // nested node with no name so it takes the parent's name instead + EXPECT_EQ("StaticCubeParent", mesh->SubMeshByIndex(1).lock()->Name()); + + // parent node containing child node with no name + // CHANGE Assimp assigns the id to the name if the mesh has no name + EXPECT_EQ("StaticCubeNestedNoName", mesh->SubMeshByIndex(2).lock()->Name()); + + // Parent of nested node with name + EXPECT_EQ("StaticCubeParent2", mesh->SubMeshByIndex(3).lock()->Name()); + + // nested node with name + EXPECT_EQ("StaticCubeNested", mesh->SubMeshByIndex(4).lock()->Name()); +} + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, MergeBoxWithDoubleSkeleton) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box_with_double_skeleton.dae")); + ASSERT_TRUE(mesh->HasSkeleton()); + auto skeleton_ptr = mesh->MeshSkeleton(); + // The two skeletons have been joined and their root is the + // animation root, called Scene + EXPECT_EQ(skeleton_ptr->RootNode()->Name(), std::string("Scene")); +} + +// For assimp below 5.2.0 mesh loading fails because of +// failing to parse the empty tag +#ifndef GZ_ASSIMP_PRE_5_2_0 +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadCylinderAnimatedFrom3dsMax) +{ + // TODO(anyone) This test shows that the mesh loads without crashing, but the + // mesh animation looks deformed when loaded. That still needs to be + // addressed. + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", + "cylinder_animated_from_3ds_max.dae")); + + EXPECT_EQ("unknown", mesh->Name()); + EXPECT_EQ(194u, mesh->VertexCount()); + EXPECT_EQ(194u, mesh->NormalCount()); + EXPECT_EQ(852u, mesh->IndexCount()); + EXPECT_LT(0u, mesh->TexCoordCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + EXPECT_EQ(1u, mesh->SubMeshCount()); + auto subMesh = mesh->SubMeshByIndex(0); + ASSERT_NE(nullptr, subMesh.lock()); + EXPECT_EQ("Cylinder01", subMesh.lock()->Name()); + + EXPECT_TRUE(mesh->HasSkeleton()); + auto skeleton = mesh->MeshSkeleton(); + ASSERT_NE(nullptr, skeleton); + ASSERT_EQ(1u, skeleton->AnimationCount()); + + auto anim = skeleton->Animation(0); + ASSERT_NE(nullptr, anim); + // TODO(luca) Fix is merged in assimp main, add when it is re-released + // EXPECT_EQ("Bone02", anim->Name()); + EXPECT_EQ(1u, anim->NodeCount()); + EXPECT_TRUE(anim->HasNode("Bone02")); +} +#endif + +///////////////////////////////////////////////// +TEST_F(AssimpLoader, LoadObjBox) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box.obj")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + EXPECT_EQ(math::Vector3d(1, 1, 1), mesh->Max()); + EXPECT_EQ(math::Vector3d(-1, -1, -1), mesh->Min()); + // 36 vertices after triangulation, assimp optimizes to 24 + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(24u, mesh->NormalCount()); + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(0u, mesh->TexCoordCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + // Make sure we can read the submesh name + EXPECT_STREQ("Cube_Cube.001", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + + EXPECT_EQ(mesh->MaterialCount(), 1u); + + const common::MaterialPtr mat = mesh->MaterialByIndex(0u); + ASSERT_TRUE(mat.get()); + + // Make sure we read the material color values + EXPECT_EQ(mat->Ambient(), math::Color(0.0, 0.0, 0.0, 1.0)); + EXPECT_EQ(mat->Diffuse(), math::Color(0.512f, 0.512f, 0.512f, 1.0f)); + EXPECT_EQ(mat->Specular(), math::Color(0.25, 0.25, 0.25, 1.0)); + EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); +} + + +///////////////////////////////////////////////// +// This tests opening an OBJ file that has an invalid material reference +TEST_F(AssimpLoader, ObjInvalidMaterial) +{ + common::AssimpLoader loader; + + std::string meshFilename = + common::testing::TestFile("data", "invalid_material.obj"); + + common::Mesh *mesh = loader.Load(meshFilename); + + EXPECT_TRUE(mesh != nullptr); +} + +///////////////////////////////////////////////// +// Open a non existing file +TEST_F(AssimpLoader, NonExistingMesh) +{ + common::AssimpLoader loader; + + std::string meshFilename = + common::testing::TestFile("data", "non_existing_mesh.glb"); + + common::Mesh *mesh = loader.Load(meshFilename); + + EXPECT_EQ(mesh->SubMeshCount(), 0); +} + +///////////////////////////////////////////////// +// This test opens a FBX file +TEST_F(AssimpLoader, LoadFbxBox) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box.fbx")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + EXPECT_EQ(math::Vector3d(100, 100, 100), mesh->Max()); + EXPECT_EQ(math::Vector3d(-100, -100, -100), mesh->Min()); + + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(24u, mesh->NormalCount()); + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(24u, mesh->TexCoordCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + // Make sure we can read the submesh name + EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + + EXPECT_EQ(mesh->MaterialCount(), 1u); + + const common::MaterialPtr mat = mesh->MaterialByIndex(0u); + ASSERT_TRUE(mat.get()); + + // Make sure we read the material color values + EXPECT_EQ(mat->Ambient(), math::Color(0.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_EQ(mat->Diffuse(), math::Color(0.8f, 0.8f, 0.8f, 1.0f)); + EXPECT_EQ(mat->Specular(), math::Color(0.8f, 0.8f, 0.8f, 1.0f)); + EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); +} + +///////////////////////////////////////////////// +// This test opens a GLB file +TEST_F(AssimpLoader, LoadGlTF2Box) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "box.glb")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + EXPECT_EQ(math::Vector3d(1, 1, 1), mesh->Max()); + EXPECT_EQ(math::Vector3d(-1, -1, -1), mesh->Min()); + + EXPECT_EQ(24u, mesh->VertexCount()); + EXPECT_EQ(24u, mesh->NormalCount()); + EXPECT_EQ(36u, mesh->IndexCount()); + EXPECT_EQ(24u, mesh->TexCoordCount()); + EXPECT_EQ(1u, mesh->SubMeshCount()); + EXPECT_EQ(1u, mesh->MaterialCount()); + + // Make sure we can read the submesh name + EXPECT_STREQ("Cube", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + + EXPECT_EQ(mesh->MaterialCount(), 1u); + + const common::MaterialPtr mat = mesh->MaterialByIndex(0u); + ASSERT_TRUE(mat.get()); + + // Make sure we read the material color values + EXPECT_EQ(mat->Ambient(), math::Color(0.4f, 0.4f, 0.4f, 1.0f)); + EXPECT_EQ(mat->Diffuse(), math::Color(0.8f, 0.8f, 0.8f, 1.0f)); + EXPECT_EQ(mat->Specular(), math::Color(0.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_DOUBLE_EQ(mat->Transparency(), 0.0); +} + +///////////////////////////////////////////////// +// Use a fully featured glb test asset, including PBR textures, emissive maps +// embedded textures, lightmaps, animations to test advanced glb features +TEST_F(AssimpLoader, LoadGlbPbrAsset) +{ + common::AssimpLoader loader; + common::Mesh *mesh = loader.Load( + common::testing::TestFile("data", "fully_featured.glb")); + + EXPECT_STREQ("unknown", mesh->Name().c_str()); + + EXPECT_EQ(mesh->SubMeshCount(), 7); + EXPECT_STREQ("Floor", mesh->SubMeshByIndex(0).lock()->Name().c_str()); + EXPECT_STREQ("SquareShelf", mesh->SubMeshByIndex(1).lock()->Name().c_str()); + EXPECT_STREQ("OpenRoboticsLogo.002", + mesh->SubMeshByIndex(2).lock()->Name().c_str()); + EXPECT_STREQ("OpenRoboticsLogo.001", + mesh->SubMeshByIndex(3).lock()->Name().c_str()); + EXPECT_STREQ("EmissiveCube", mesh->SubMeshByIndex(4).lock()->Name().c_str()); + EXPECT_STREQ("OpenCola", mesh->SubMeshByIndex(5).lock()->Name().c_str()); + EXPECT_STREQ("OpenRoboticsLogo", + mesh->SubMeshByIndex(6).lock()->Name().c_str()); + + // Emissive cube has an embedded emissive texture + auto materialId = mesh->SubMeshByIndex(4).lock()->GetMaterialIndex(); + ASSERT_TRUE(materialId.has_value()); + auto material = mesh->MaterialByIndex(materialId.value()); + ASSERT_NE(material, nullptr); + auto pbr = material->PbrMaterial(); + ASSERT_NE(pbr, nullptr); + EXPECT_NE(pbr->EmissiveMapData(), nullptr); + + // SquareShelf has full PBR textures, including metallicroughness + // and ambient occlusion + materialId = mesh->SubMeshByIndex(1).lock()->GetMaterialIndex(); + ASSERT_TRUE(materialId.has_value()); + material = mesh->MaterialByIndex(materialId.value()); + ASSERT_NE(material, nullptr); + pbr = material->PbrMaterial(); + ASSERT_NE(pbr, nullptr); + + // Check the texture data itself + auto img = material->TextureData(); + ASSERT_NE(img, nullptr); + EXPECT_EQ(img->Width(), 512); + EXPECT_EQ(img->Height(), 512); + // A black and a white pixel + EXPECT_EQ(img->Pixel(0, 0), math::Color(0.0f, 0.0f, 0.0f, 1.0f)); + EXPECT_EQ(img->Pixel(100, 100), math::Color(1.0f, 1.0f, 1.0f, 1.0f)); + + + EXPECT_NE(pbr->NormalMapData(), nullptr); + // Metallic roughness and alpha from textures only works in assimp > 5.2.0 +#ifndef GZ_ASSIMP_PRE_5_2_0 + // Alpha from textures + EXPECT_TRUE(material->TextureAlphaEnabled()); + EXPECT_TRUE(material->TwoSidedEnabled()); + EXPECT_EQ(material->AlphaThreshold(), 0.5); + // Metallic and roughness maps + EXPECT_NE(pbr->MetalnessMapData(), nullptr); + EXPECT_NE(pbr->RoughnessMapData(), nullptr); + // Check pixel values to test metallicroughness texture splitting + EXPECT_FLOAT_EQ(pbr->MetalnessMapData()->Pixel(256, 256).R(), 0.0); + EXPECT_FLOAT_EQ(pbr->RoughnessMapData()->Pixel(256, 256).R(), 124.0 / 255.0); + // Bug in assimp 5.0.x that doesn't parse coordinate sets properly + EXPECT_EQ(pbr->LightMapTexCoordSet(), 1); +#endif + EXPECT_NE(pbr->LightMapData(), nullptr); + + // Mesh has 3 animations + auto skel = mesh->MeshSkeleton(); + ASSERT_NE(skel, nullptr); + ASSERT_EQ(skel->AnimationCount(), 3); + EXPECT_STREQ("Action1", skel->Animation(0)->Name().c_str()); + EXPECT_STREQ("Action2", skel->Animation(1)->Name().c_str()); + EXPECT_STREQ("Action3", skel->Animation(2)->Name().c_str()); +} diff --git a/graphics/src/CMakeLists.txt b/graphics/src/CMakeLists.txt index 473caf9a4..aa832387e 100644 --- a/graphics/src/CMakeLists.txt +++ b/graphics/src/CMakeLists.txt @@ -16,6 +16,7 @@ target_link_libraries(${graphics_target} gz-math${GZ_MATH_VER}::gz-math${GZ_MATH_VER} gz-utils${GZ_UTILS_VER}::gz-utils${GZ_UTILS_VER} PRIVATE + ${GzAssimp_LIBRARIES} GTS::GTS FreeImage::FreeImage) @@ -27,6 +28,17 @@ gz_build_tests( gz-common${GZ_COMMON_VER}-testing ) +# Assimp doesn't offer preprocessor version, use cmake to set a compatibility +# mode for versions below 5.2.0 +if(${GzAssimp_VERSION} STRLESS "5.2.0") + message("Warning, assimp below 5.2.0 detected, setting compatibility mode") + target_compile_definitions(${graphics_target} PRIVATE GZ_ASSIMP_PRE_5_2_0) + if(TARGET UNIT_AssimpLoader_TEST) + target_compile_definitions(UNIT_AssimpLoader_TEST PRIVATE GZ_ASSIMP_PRE_5_2_0) + endif() +endif() + + if(USE_EXTERNAL_TINYXML2) # If we are using an external copy of tinyxml2, add its imported target diff --git a/graphics/src/Material.cc b/graphics/src/Material.cc index 79e3ab34f..60469be85 100644 --- a/graphics/src/Material.cc +++ b/graphics/src/Material.cc @@ -40,6 +40,9 @@ class gz::common::Material::Implementation /// \brief the texture image file name public: std::string texImage; + /// \brief Texture raw data + public: std::shared_ptr texData; + /// \brief the ambient light color public: math::Color ambient; @@ -138,9 +141,17 @@ std::string Material::Name() const } ////////////////////////////////////////////////// -void Material::SetTextureImage(const std::string &_tex) +void Material::SetTextureImage(const std::string &_tex, + const std::shared_ptr &_img) { this->dataPtr->texImage = _tex; + this->dataPtr->texData = _img; +} + +////////////////////////////////////////////////// +std::shared_ptr Material::TextureData() const +{ + return this->dataPtr->texData; } ////////////////////////////////////////////////// @@ -148,6 +159,7 @@ void Material::SetTextureImage(const std::string &_tex, const std::string &_resourcePath) { this->dataPtr->texImage = common::joinPaths(_resourcePath, _tex); + this->dataPtr->texData = nullptr; // If the texture image doesn't exist then try the next most likely path. if (!exists(this->dataPtr->texImage)) diff --git a/graphics/src/Material_TEST.cc b/graphics/src/Material_TEST.cc index 74fbb961f..a3f34d5b9 100644 --- a/graphics/src/Material_TEST.cc +++ b/graphics/src/Material_TEST.cc @@ -16,6 +16,7 @@ #include +#include "gz/common/Image.hh" #include "gz/common/Material.hh" #include "gz/common/Pbr.hh" @@ -32,9 +33,12 @@ TEST_F(MaterialTest, Material) EXPECT_TRUE(mat.Ambient() == math::Color(1.0f, 0.5f, 0.2f, 1.0f)); EXPECT_TRUE(mat.Diffuse() == math::Color(1.0f, 0.5f, 0.2f, 1.0f)); EXPECT_STREQ("gz_material_0", mat.Name().c_str()); + EXPECT_EQ(nullptr, mat.TextureData()); - mat.SetTextureImage("texture_image"); + auto texImg = std::make_shared(); + mat.SetTextureImage("texture_image", texImg); EXPECT_STREQ("texture_image", mat.TextureImage().c_str()); + EXPECT_EQ(texImg, mat.TextureData()); mat.SetTextureImage("texture_image", "/path"); std::string texturePath = common::joinPaths("/path", "..", diff --git a/graphics/src/MeshManager.cc b/graphics/src/MeshManager.cc index 890688eb1..085dbb623 100644 --- a/graphics/src/MeshManager.cc +++ b/graphics/src/MeshManager.cc @@ -18,7 +18,8 @@ #include #include #include -#include +#include +#include #include #ifndef _WIN32 @@ -29,6 +30,7 @@ #include "gz/common/Console.hh" #include "gz/common/Mesh.hh" #include "gz/common/SubMesh.hh" +#include "gz/common/AssimpLoader.hh" #include "gz/common/ColladaLoader.hh" #include "gz/common/ColladaExporter.hh" #include "gz/common/OBJLoader.hh" @@ -58,14 +60,20 @@ class gz::common::MeshManager::Implementation /// \brief 3D mesh loader for OBJ files public: OBJLoader objLoader; + /// \brief 3D mesh loader for Assimp assets (others) + public: AssimpLoader assimpLoader; + /// \brief Dictionary of meshes, indexed by name - public: std::map meshes; + public: std::unordered_map meshes; /// \brief supported file extensions for meshes - public: std::vector fileExtensions; + public: std::unordered_set fileExtensions; /// \brief Mutex to protect the mesh map public: std::mutex mutex; + + /// \brief True if assimp is used for loading all supported mesh formats + public: bool forceAssimp; #ifdef _WIN32 #pragma warning(pop) #endif @@ -97,9 +105,13 @@ MeshManager::MeshManager() this->CreateTube("selection_tube", 1.0f, 1.2f, 0.01f, 1, 64); - this->dataPtr->fileExtensions.push_back("stl"); - this->dataPtr->fileExtensions.push_back("dae"); - this->dataPtr->fileExtensions.push_back("obj"); + this->dataPtr->fileExtensions.insert("stl"); + this->dataPtr->fileExtensions.insert("dae"); + this->dataPtr->fileExtensions.insert("obj"); + this->dataPtr->fileExtensions.insert("gltf"); + this->dataPtr->fileExtensions.insert("glb"); + this->dataPtr->fileExtensions.insert("fbx"); + } ////////////////////////////////////////////////// @@ -137,19 +149,27 @@ const Mesh *MeshManager::Load(const std::string &_filename) std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); MeshLoader *loader = nullptr; - - if (extension == "stl" || extension == "stlb" || extension == "stla") - loader = &this->dataPtr->stlLoader; - else if (extension == "dae") - loader = &this->dataPtr->colladaLoader; - else if (extension == "obj") - loader = &this->dataPtr->objLoader; + this->SetAssimpEnvs(); + if (this->dataPtr->forceAssimp) + { + loader = &this->dataPtr->assimpLoader; + } else { - gzerr << "Unsupported mesh format for file[" << _filename << "]\n"; - return nullptr; + if (extension == "stl" || extension == "stlb" || extension == "stla") + loader = &this->dataPtr->stlLoader; + else if (extension == "dae") + loader = &this->dataPtr->colladaLoader; + else if (extension == "obj") + loader = &this->dataPtr->objLoader; + else if (extension == "gltf" || extension == "glb" || extension == "fbx") + loader = &this->dataPtr->assimpLoader; + else + { + gzerr << "Unsupported mesh format for file[" << _filename << "]\n"; + return nullptr; + } } - // This mutex prevents two threads from loading the same mesh at the // same time. std::lock_guard lock(this->dataPtr->mutex); @@ -199,8 +219,7 @@ bool MeshManager::IsValidFilename(const std::string &_filename) std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); - return std::find(this->dataPtr->fileExtensions.begin(), - this->dataPtr->fileExtensions.end(), extension) != + return this->dataPtr->fileExtensions.find(extension) != this->dataPtr->fileExtensions.end(); } @@ -231,9 +250,7 @@ void MeshManager::AddMesh(Mesh *_mesh) ////////////////////////////////////////////////// const Mesh *MeshManager::MeshByName(const std::string &_name) const { - std::map::const_iterator iter; - - iter = this->dataPtr->meshes.find(_name); + auto iter = this->dataPtr->meshes.find(_name); if (iter != this->dataPtr->meshes.end()) return iter->second; @@ -274,8 +291,7 @@ bool MeshManager::HasMesh(const std::string &_name) const if (_name.empty()) return false; - std::map::const_iterator iter; - iter = this->dataPtr->meshes.find(_name); + auto iter = this->dataPtr->meshes.find(_name); return iter != this->dataPtr->meshes.end(); } @@ -1613,3 +1629,16 @@ void MeshManager::ConvertPolylinesToVerticesAndEdges( } } } + +////////////////////////////////////////////////// +void MeshManager::SetAssimpEnvs() +{ + std::string forceAssimpEnv; + common::env("GZ_MESH_FORCE_ASSIMP", forceAssimpEnv); + this->dataPtr->forceAssimp = false; + if (forceAssimpEnv == "true") + { + gzmsg << "Using assimp to load all mesh formats" << std::endl; + this->dataPtr->forceAssimp = true; + } +} diff --git a/graphics/src/Pbr.cc b/graphics/src/Pbr.cc index 6d84d8717..d17d1ee52 100644 --- a/graphics/src/Pbr.cc +++ b/graphics/src/Pbr.cc @@ -32,6 +32,9 @@ class gz::common::Pbr::Implementation /// \brief Normal map public: std::string normalMap = ""; + /// \brief Pointer containing the normal map data, if loaded from memory + public: std::shared_ptr normalMapData = nullptr; + /// \brief Normal map space public: NormalMapSpace normalMapSpace = NormalMapSpace::TANGENT; @@ -44,15 +47,31 @@ class gz::common::Pbr::Implementation /// \brief Roughness map (metal workflow only) public: std::string roughnessMap = ""; + /// \brief Pointer containing the roughness map data, + /// if loaded from memory + public: std::shared_ptr roughnessMapData = nullptr; + /// \brief Metalness map (metal workflow only) public: std::string metalnessMap = ""; + /// \brief Pointer containing the metalness map data, + /// if loaded from memory + public: std::shared_ptr metalnessMapData = nullptr; + /// \brief Emissive map public: std::string emissiveMap = ""; + /// \brief Pointer containing the emissive map data, + /// if loaded from memory + public: std::shared_ptr emissiveMapData = nullptr; + /// \brief Light map public: std::string lightMap; + /// \brief Pointer containing the light map data, + /// if loaded from memory + public: std::shared_ptr lightMapData = nullptr; + /// \brief Light map texture coordinate set public: unsigned int lightMapUvSet = 0u; @@ -93,11 +112,17 @@ bool Pbr::operator==(const Pbr &_pbr) const { return (this->dataPtr->albedoMap == _pbr.dataPtr->albedoMap) && (this->dataPtr->normalMap == _pbr.dataPtr->normalMap) + && (this->dataPtr->normalMapData == _pbr.dataPtr->normalMapData) && (this->dataPtr->metalnessMap == _pbr.dataPtr->metalnessMap) + && (this->dataPtr->metalnessMapData == _pbr.dataPtr->metalnessMapData) && (this->dataPtr->roughnessMap == _pbr.dataPtr->roughnessMap) + && (this->dataPtr->roughnessMapData == _pbr.dataPtr->roughnessMapData) && (this->dataPtr->glossinessMap == _pbr.dataPtr->glossinessMap) && (this->dataPtr->environmentMap == _pbr.dataPtr->environmentMap) && (this->dataPtr->emissiveMap == _pbr.dataPtr->emissiveMap) + && (this->dataPtr->emissiveMapData == _pbr.dataPtr->emissiveMapData) + && (this->dataPtr->lightMap == _pbr.dataPtr->lightMap) + && (this->dataPtr->lightMapData == _pbr.dataPtr->lightMapData) && (this->dataPtr->ambientOcclusionMap == _pbr.dataPtr->ambientOcclusionMap) && (gz::math::equal( @@ -133,10 +158,18 @@ NormalMapSpace Pbr::NormalMapType() const } ////////////////////////////////////////////////// -void Pbr::SetNormalMap(const std::string &_map, NormalMapSpace _space) +std::shared_ptr Pbr::NormalMapData() const +{ + return this->dataPtr->normalMapData; +} + +////////////////////////////////////////////////// +void Pbr::SetNormalMap(const std::string &_map, NormalMapSpace _space, + const std::shared_ptr &_img) { this->dataPtr->normalMap = _map; this->dataPtr->normalMapSpace = _space; + this->dataPtr->normalMapData = _img; } ////////////////////////////////////////////////// @@ -170,9 +203,17 @@ std::string Pbr::RoughnessMap() const } ////////////////////////////////////////////////// -void Pbr::SetRoughnessMap(const std::string &_map) +std::shared_ptr Pbr::RoughnessMapData() const +{ + return this->dataPtr->roughnessMapData; +} + +////////////////////////////////////////////////// +void Pbr::SetRoughnessMap(const std::string &_map, + const std::shared_ptr &_img) { this->dataPtr->roughnessMap = _map; + this->dataPtr->roughnessMapData = _img; } ////////////////////////////////////////////////// @@ -182,9 +223,17 @@ std::string Pbr::MetalnessMap() const } ////////////////////////////////////////////////// -void Pbr::SetMetalnessMap(const std::string &_map) +std::shared_ptr Pbr::MetalnessMapData() const +{ + return this->dataPtr->metalnessMapData; +} + +////////////////////////////////////////////////// +void Pbr::SetMetalnessMap(const std::string &_map, + const std::shared_ptr &_img) { this->dataPtr->metalnessMap = _map; + this->dataPtr->metalnessMapData = _img; } ////////////////////////////////////////////////// @@ -254,9 +303,17 @@ std::string Pbr::EmissiveMap() const } ////////////////////////////////////////////////// -void Pbr::SetEmissiveMap(const std::string &_map) +std::shared_ptr Pbr::EmissiveMapData() const +{ + return this->dataPtr->emissiveMapData; +} + +////////////////////////////////////////////////// +void Pbr::SetEmissiveMap(const std::string &_map, + const std::shared_ptr &_img) { this->dataPtr->emissiveMap = _map; + this->dataPtr->emissiveMapData = _img; } ////////////////////////////////////////////////// @@ -266,10 +323,18 @@ std::string Pbr::LightMap() const } ////////////////////////////////////////////////// -void Pbr::SetLightMap(const std::string &_map, unsigned int _uvSet) +std::shared_ptr Pbr::LightMapData() const +{ + return this->dataPtr->lightMapData; +} + +////////////////////////////////////////////////// +void Pbr::SetLightMap(const std::string &_map, unsigned int _uvSet, + const std::shared_ptr &_img) { this->dataPtr->lightMap = _map; this->dataPtr->lightMapUvSet = _uvSet; + this->dataPtr->lightMapData = _img; } ////////////////////////////////////////////////// diff --git a/graphics/src/Pbr_TEST.cc b/graphics/src/Pbr_TEST.cc index 361049d8c..30de47473 100644 --- a/graphics/src/Pbr_TEST.cc +++ b/graphics/src/Pbr_TEST.cc @@ -18,6 +18,7 @@ #include #include +#include "gz/common/Image.hh" #include "gz/common/Pbr.hh" ///////////////////////////////////////////////// @@ -28,12 +29,17 @@ TEST(Pbr, BasicAPI) EXPECT_EQ(gz::common::PbrType::NONE, pbr.Type()); EXPECT_EQ(std::string(), pbr.AlbedoMap()); EXPECT_EQ(std::string(), pbr.NormalMap()); + EXPECT_EQ(nullptr, pbr.NormalMapData()); EXPECT_EQ(gz::common::NormalMapSpace::TANGENT, pbr.NormalMapType()); EXPECT_EQ(std::string(), pbr.RoughnessMap()); + EXPECT_EQ(nullptr, pbr.RoughnessMapData()); EXPECT_EQ(std::string(), pbr.MetalnessMap()); + EXPECT_EQ(nullptr, pbr.MetalnessMapData()); EXPECT_EQ(std::string(), pbr.EmissiveMap()); + EXPECT_EQ(nullptr, pbr.EmissiveMapData()); EXPECT_EQ(std::string(), pbr.LightMap()); EXPECT_EQ(0u, pbr.LightMapTexCoordSet()); + EXPECT_EQ(nullptr, pbr.LightMapData()); EXPECT_DOUBLE_EQ(0.5, pbr.Roughness()); EXPECT_DOUBLE_EQ(0.0, pbr.Metalness()); EXPECT_EQ(std::string(), pbr.SpecularMap()); @@ -49,8 +55,11 @@ TEST(Pbr, BasicAPI) pbr.SetAlbedoMap("metal_albedo_map.png"); EXPECT_EQ("metal_albedo_map.png", pbr.AlbedoMap()); - pbr.SetNormalMap("metal_normal_map.png"); + auto normalImg = std::make_shared(); + pbr.SetNormalMap("metal_normal_map.png", gz::common::NormalMapSpace::TANGENT, + normalImg); EXPECT_EQ("metal_normal_map.png", pbr.NormalMap()); + EXPECT_EQ(normalImg, pbr.NormalMapData()); pbr.SetEnvironmentMap("metal_env_map.png"); EXPECT_EQ("metal_env_map.png", pbr.EnvironmentMap()); @@ -59,18 +68,26 @@ TEST(Pbr, BasicAPI) EXPECT_EQ("metal_ambient_occlusion_map.png", pbr.AmbientOcclusionMap()); - pbr.SetEmissiveMap("metal_emissive_map.png"); + auto emissiveImg = std::make_shared(); + pbr.SetEmissiveMap("metal_emissive_map.png", emissiveImg); EXPECT_EQ("metal_emissive_map.png", pbr.EmissiveMap()); + EXPECT_EQ(emissiveImg, pbr.EmissiveMapData()); - pbr.SetLightMap("metal_light_map.png", 1u); + auto lightImg = std::make_shared(); + pbr.SetLightMap("metal_light_map.png", 1u, lightImg); EXPECT_EQ("metal_light_map.png", pbr.LightMap()); EXPECT_EQ(1u, pbr.LightMapTexCoordSet()); + EXPECT_EQ(lightImg, pbr.LightMapData()); - pbr.SetRoughnessMap("roughness_map.png"); + auto roughnessImg = std::make_shared(); + pbr.SetRoughnessMap("roughness_map.png", roughnessImg); EXPECT_EQ("roughness_map.png", pbr.RoughnessMap()); + EXPECT_EQ(roughnessImg, pbr.RoughnessMapData()); - pbr.SetMetalnessMap("metalness_map.png"); + auto metalnessImg = std::make_shared(); + pbr.SetMetalnessMap("metalness_map.png", metalnessImg); EXPECT_EQ("metalness_map.png", pbr.MetalnessMap()); + EXPECT_EQ(metalnessImg, pbr.MetalnessMapData()); pbr.SetRoughness(0.8); EXPECT_DOUBLE_EQ(0.8, pbr.Roughness()); diff --git a/test/data/box.fbx b/test/data/box.fbx new file mode 100644 index 000000000..edf8d0d25 Binary files /dev/null and b/test/data/box.fbx differ diff --git a/test/data/box.glb b/test/data/box.glb new file mode 100644 index 000000000..42943474f Binary files /dev/null and b/test/data/box.glb differ diff --git a/test/data/fully_featured.glb b/test/data/fully_featured.glb new file mode 100644 index 000000000..f72ce4b50 Binary files /dev/null and b/test/data/fully_featured.glb differ diff --git a/test/integration/mesh.cc b/test/integration/mesh.cc index 3b8abd7ce..c13fadac8 100644 --- a/test/integration/mesh.cc +++ b/test/integration/mesh.cc @@ -134,10 +134,30 @@ TEST_F(MeshTest, Load) // Loading should be successful EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.dae")); EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.obj")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.fbx")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.glb")); // Reloading should not cause errors EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.dae")); EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.obj")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.fbx")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.glb")); + + // Forget about previously loaded meshes + EXPECT_TRUE(common::MeshManager::Instance()->RemoveMesh("box.dae")); + EXPECT_TRUE(common::MeshManager::Instance()->RemoveMesh("box.obj")); + EXPECT_TRUE(common::MeshManager::Instance()->RemoveMesh("box.fbx")); + EXPECT_TRUE(common::MeshManager::Instance()->RemoveMesh("box.glb")); + + // When forcing assimp, loading should still be successful for all formats + common::setenv("GZ_MESH_FORCE_ASSIMP", "true"); + common::MeshManager::Instance()->SetAssimpEnvs(); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.dae")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.obj")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.fbx")); + EXPECT_NE(nullptr, common::MeshManager::Instance()->Load("box.glb")); + EXPECT_EQ(nullptr, common::MeshManager::Instance()->Load("break.xml")); + common::unsetenv("GZ_MESH_FORCE_ASSIMP"); } /////////////////////////////////////////////////